This mini CRUD project was to have a play with the techstack:
- React
- Typescript
- Redux
- Jest
Screen.Recording.2026-07-20.113917.mp4
- cd .\src\api\mock-api\
- json-server --watch db.json --port 3000
- npm run dev
Explore Tool is a React + TypeScript travel planning application that allows users to:
- View trips
- Create new trips
- Delete trips
- Update existing trips
- View individual trip pages
- Edit trip details
- Manage itinerary and packing list items
The application uses a modern frontend architecture:
- React for UI components
- TypeScript for type safety
- Redux Toolkit for global state management
- Redux Thunk (
createAsyncThunk) for asynchronous API calls - Axios for HTTP requests
- JSON Server as a temporary backend
- Jest for testing
React Component
|
|
dispatch(action)
|
|
Redux Async Thunk
|
|
API Layer (Axios)
|
|
Backend / JSON Server
|
|
Response
|
|
Reducer updates Redux Store
|
|
React re-renders from state
The application separates responsibilities:
src
|
├── api
| └── tripApi.ts # HTTP requests
|
├── components
| ├── TripCard.tsx # Trip preview card
| ├── EditableField.tsx # Reusable editable text
| └── TodoComponent.tsx # Itinerary/packing lists
|
├── pages
| ├── HomePage.tsx
| ├── TripPage.tsx
| └── AddTripModal.tsx
|
├── state
| ├── store.ts
| └── tripSlice.ts
|
└── tests
└── tripSlice.test.ts
Trip data is represented using a shared interface.
Example:
export interface Trip {
id: string;
title: string;
description: string;
date: string;
itineraryItems: TodoItem[];
packingList: TodoItem[];
image?: string;
}Optional properties are used when values are not always available.
For example:
image?: string;because newly created trips may not immediately have an image.
A separate type was created for creating trips:
export type CreateTripRequest = Omit<Trip, "id">;Reason:
The frontend does not generate IDs.
The backend generates IDs when a POST request is made.
Example:
Frontend sends:
{
"title": "Surf Trip",
"description": "A surfing adventure"
}Backend responds:
{
"id": "abc123",
"title": "Surf Trip",
"description": "A surfing adventure"
}This prevents accidentally sending fake IDs.
The API layer is responsible only for communication.
Example:
const postTrip = async (trip: CreateTripRequest): Promise<Trip> => {
const response = await axios.post(API_BASE_URL, trip);
return response.data;
};The API layer does not modify Redux state.
Its only responsibility:
Request -> Response
Redux stores application-wide state.
Current state:
interface TripState {
trips: Trip[];
loading: boolean;
error: string | null;
}Example:
{
trips: [
{
id: "123",
title: "Morocco"
}
],
loading: false,
error: null
}Created using Redux Toolkit:
createSlice();A slice combines:
- State
- Reducers
- Actions
Example:
const tripSlice = createSlice({
name: "trip",
initialState,
reducers: {},
extraReducers: (builder) => {},
});Originally:
reducers: {
}seemed unusual.
However, the application uses API-driven updates.
Therefore, state changes are triggered by asynchronous requests.
These are handled using:
extraReducers;rather than manual reducers.
Async thunks handle API calls.
Example:
export const fetchTripsAsync = createAsyncThunk("trip/fetchTrips", async () => {
const trips = await fetchTrips();
return trips;
});The thunk lifecycle:
dispatch(fetchTripsAsync())
|
v
pending
|
v
API request
|
v
fulfilled OR rejected
Extra reducers listen for thunk results.
Example:
builder.addCase(fetchTripsAsync.fulfilled, (state, action) => {
state.trips = action.payload;
});Meaning:
When
fetchTripsAsyncsucceeds, replace trips in Redux with the returned data.
state.loading = true;The UI can show a spinner.
state.trips = action.payload;
state.loading = false;The API response becomes Redux state.
state.error = action.error.message;The UI can display an error.
Flow:
User submits modal
|
v
dispatch(postTripAsync)
|
v
POST request
|
v
Backend generates ID
|
v
fulfilled reducer
|
v
state.trips.push(newTrip)
Reducer:
state.trips.push(action.payload);Flow:
User clicks delete
|
v
dispatch(deleteTripAsync(id))
|
v
DELETE request
|
v
fulfilled
|
v
remove from Redux
Reducer:
state.trips = state.trips.filter((trip) => trip.id !== action.payload);Reducer:
const index = state.trips.findIndex((trip) => trip.id === action.payload.id);
state.trips[index] = action.payload;Reason:
Find the existing object and replace it with the updated API response.
Important design decision:
Examples:
- Trips
- Trip details
- Saved itinerary items
Examples:
- Is modal open?
- Is text field being edited?
- Current input value
Originally:
TripHeader
|
owns title state
Problem:
TripPage could not save changes.
Solution:
Lift state upward.
New pattern:
TripPage
owns:
editedTrip state
|
|
TripHeader
|
|
EditableField
The child receives:
value;
onChange;Example:
<EditableField
value={editedTrip.title}
onChange={updateTitle}
/>This keeps the source of truth in TripPage.
Originally:
TodoComponent owns todos
Problem:
Parent cannot save changes.
Updated design:
TripPage
|
owns itinerary state
|
TodoComponent
TodoComponent receives:
todos;
onChange;This allows:
- Add item
- Remove item
- Toggle completion
while keeping changes inside the editable trip object.
Jest was added with:
- Jest
- ts-jest
- Testing Library
Tests currently focus on Redux behaviour.
The main pattern:
Arrange
Create initial state
Act
Dispatch thunk
Assert
Check resulting state
Example:
expect(nextState.trips).toHaveLength(1);
expect(nextState.loading).toBe(false);
expect(nextState.error).toBe(null);High priority.
Test:
- Fetch success
- Fetch failure
- Create trip
- Delete trip
- Update trip
Using React Testing Library.
Examples:
Test:
- User enters values
- Clicks submit
- Dispatch occurs
Test:
- Correct title displays
- Delete button calls delete action
Test:
- Loads trip
- Updates fields
- Save button dispatches update thunk
Lower priority.
Mock Axios requests.
Example:
axios.post
returns fake response
expect(postTrip())
returns expected trip
Currently:
Refresh page
Redux resets
Fetch again
Possible solutions:
- Fetch trip on page load
- Redux Persist
- React Query
JSON Server is currently used for development.
Future stack:
- Spring Boot backend
- PostgreSQL database
- Authentication
- Cloud storage for images
- Redux should store shared application data, not every piece of state.
- Async thunks connect API calls to Redux state changes.
- Reducers describe how state changes after events.
- Controlled components make complex forms easier to manage.
- Parent components should own state when multiple children need access.
- TypeScript prevents many runtime bugs before execution.
- Good architecture makes adding features easier.