Example: https://blog.logrocket.com/handling-user-authentication-redux-toolkit/
Below are the terms that describe the structure of Redux.
A global object that store the state Created by using function createStore()
- Input: reducer
- Output: state object that has 3 API
- store.dispatch: dispatch an action
- store.subscribe
- store.getState
An object that has two properties
- Type
- Payload
You can think of an action as an event that describes something.
What: Function that return an action object
const increment = () => {
return { type: 'counter/increment' };
};
Why:
- Writting action object is repetitive, tedious and error-prone.
- Useful when dispatch the same action in multiple place
- Store API. The only way to update the state is to call
store.dispatch()
.- Input: an action object
- Output: void
- Store dispatches an action to return the new state.
- You can think of dispatching actions as "triggering event":
store.dispatch( increment() )
A function that calculates new state based on previouse state and action
- Input: global state object and action
- Ouput: new state
- Signature:
(state, action) => newState
You can think of an reducer as an event listener which handles an event based on the received action (event) type. Reducer must not do any asynchronous logic, cause other side effects. Side effects is any changes that is outside the functions:
- Making AJAX HTTP request
- Generating random number or unique random IDs
- Logging a value to console Reducer must not mutate the state, only copy the current state then makes changes to that copy. Reducer must not calculate random values, because it makes the results unpredictable
Tips Normally, we have reducers for fetching data, fetching data successfully, fetching data fail. If there is only one data state to update, we can add that data update to the fetching data success. Otherwise, we will have a seperate reducer for updating the data state.
- Initial setup:
- A Redux store is created using a root reducer function.
- The store calls the root reducer once, saves the return value as its initial state
- All the UI components that has access to the current state of the store use that data to render
- Updates:
- Some events happens, like user clicking a button
- Redux dispatches an action to the store
- Store run the reducer function again with the previous state and the current aciton, and saves the return value as the new state
- Store notifies all the components that are subscribed about the update
- Each components check if the part of the state they need have changed, if so it re-render to update the UI.
A slice is a collection of Redux reducer and actions for a single features
Function to create store
- Input: an object that has properties. One of them is reducer.
- reducer: object(root reducer) that take feature name as properties feature_1_name: reducer_of_feature_1 feature_2_name: reducer_of_feature_2
- Output: store that is global state object
const store = configureStore({ reducer: { counter: counterReducer } });
Here we have a counter property of state object and couterReducer function to be in charge of that property.
A function that automatically generate action creators and action types that correspond to the reducers and state. Internally, it uses createAction
and createReducer
.
- Input: object
name
: name of the sliceinitialState
: initial state objectreducers
: object that includes reducers functions.key: (state, action) => newState
name
andkey
will be used to generate string action type constant:name/key
todoSlice.actions[key]
is the name an action creator that returns action object:{type: "name/key"}
- If we pass payload as the argument, it will return as
{type: "name/key", payload }
- This action creator has a 'type' property. You can get the type directly without invoking it.
- If we pass payload as the argument, it will return as
You can write your reducer function like this:
reducer_function: {
reducer(state, action) { return state },
prepare( args ) { return { payload: {args} } }
}
Why:
- Do some 'preparation' logic like generating unique ID
- pass in multiple parameters How:
- call the action creator (
reducer_function
above) - prepare function called with whatever parameters were passed in and return the payload
- The payload is passed in the action in the 'reducer' function.
- Create a Redux store with
configureStore
configureStore
accepts a reducer function as a named argumentconfigureStore
automatically sets up the store with good default settings - Provide the Redux store to the React application components
- Put a React-Redux
<Provider>
component around your<App />
- Pass the Redux store as
<Provider store={store}>
- Create a Redux "slice" reducer with createSlice
- Put a React-Redux
- Call
createSlice
with a string name, an initial state, and named reducer functions- Reducer functions may "mutate" the state using Immer
- Export default the generated slice reducer and export action creators
- Use the React-Redux
useSelector
anduseDispatch
hooks in React components- Read data from the store with the
useSelector
hook - Get the dispatch function with the
useDispatch
hook, and dispatch actions as needed
- Read data from the store with the
What:
- The middle thing between UI (dispatch action) and the store (receive action)
- Connects to API, receive data then dispatch the real action to the store
UI => dispatch() => middleware => dispatch(storeAction) => run the reducer
Why: Reducers are supposed to be pure. They don't change anything outside its scope, or do any API calls. If you want to work with any APIs, you will need a middleware. Where: - Normal flow:
- An event occurs
- Dispatch an action
- Reducer creates a new state from the change prescribed by the action
- New state is passed into the React app via props
- With middleware:
- An event occurs
- Dispatch an action
- Redux notifies all the middleware that there is a new action.
- Middlewares receives the action, do something side-effect like setTimeout or other async logic, then dispatch another action
- Reducer creates a new state from the change prescribed by the action
- New state is passed into the React app via props
What:
- A function that wraps an expression to delay its evaluation
let x = 1 + 2; // x is calculated and return immediately.
let foo = () => 1 + 2; // foo delayed the calculation, foo is a thunk.
- Not normal function, is a function that returned by another function
There are 2 ways to create redux thunk: createAsyncThunk
and manually.
export const incrementAsync =
(amount: number): AppThunk =>
async (dispatch, getState) => {
try {
const response = await fetchCount(amount)
dispatch(incrementByAmount(response.data))
} catch (error) {
console.log("error", error)
}
}
// Usage
<button onClick={() => dispatch(incrementAsync(amount))}>Increment Async</button>
- A Redux Middleware that looks at every actions, if it's a function, call that function.
- Delay the dispatch of an action of dispatch only if a certain condition is met
- Action creators now return a function instead of an object. That Inner function is a thunk:
- Input:
- dispatch: to dispatch the new action if they need
- getState: to access the current state.
- Output: void.
- Input:
When we make an API call, its progress can be in one of 4 states:
- idle: The request hasn't started yet
- loading: The request is in progress
- succeeded: The request succeeded, and we now have the data we need
- failed: The request failed, and there's probably an error message
createAsyncThunk
will automatically creates action types and action creators for the 'loading/succeeded/failed' status.
Then it automatically dispatchs those actions relatively based on the resulting Promise. It means, after dispatching 'loading', it continues dispatching 'succeeded' ( fullfilled ) status if success, or 'failed' (rejected) if failure
Input:
- prefix for aciton types: 'posts/fetchPosts' will be prefix of action types: 'posts/fetchPosts/pending'
- payload creator: a function that make API calls
- Input: the params that passed in the action creator when we dispatch it:
dispatch(addTodo(text)) // text will be the param of payload creator
- Output: Promise with data or Promise with Error (async always return Promise)
- Input: the params that passed in the action creator when we dispatch it:
Output: action creators and their action types:
- fetchPosts.pending: todos/fetchPosts/pending
- fetchPosts.fulfilled: todos/fetchPosts/fulfilled
- fetchPosts.rejected: todos/fetchPosts/rejected
If success, createAsyncThunk
will extract data from API response and pass that data returned from the payload creator into payload
When we dispatch an asynchronous action that created by createAsyncThunk
above
- Run async_action_name.pending reducer
- If success, run async_action_name.fullfilled reducer
- Else if failure, run async_action_name.reject reducer
These reducers are extraReducers:
[ async_action_name.pending ]: ( state, action ) => {
state.status = 'loading';
},
[ async_action_name.fullfilled ]: ( state, action ) => {
state.status = 'succeeded';
state.posts = state.posts.concat( action.payload );
},
createSelector
Get modified data from other selector
Selector:
- What: is any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state.
- Name convention: prefix with select word, then combine with the description of value being selected: selectTodoById, selectFilteredTodos
- Where: use as
useSelector
argument:const todos = useSelector( selectFilteredTodos )
// Input selectors (selector: the function passed to useSelector)
const selectItems = (state) => state.items;
const selectItemId = (state, itemId) => itemId;
// Output selector will take the result from **all** the input selectors as arguments
// And return final result value
// The final result will be cached for the next time
const selectItemById = createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
);
//Then this selector is passed into useSelector
const itemIds = useSelector(selectItemById);
Why:
Normal selector like: state => state.todos
always return the new reference. As a result, the component that uses the useSelector
to be re-rendered
Notes:
createSelector
only helpful when derive additional values from original data. If you are just looking up or returning existing data, you can keep the selector as plain function
useDispatch
: dispatch an action
Input: global state Output: return dispatch function
useSelector
: read data from store Input: a selector function: (state) => value_from_state- Input: store state
- Output: value based on the state. Ouput: the returned value from the selector above
useSelector
subscribes to the store, and re-runs the selector after every action is dispatched
If the value returned by the selector changes from the last time it runs, useSelector will force the component to re-render with the new data
Note: if the value returned by the selector is a new reference, the component is re-render too
// Bad: always returning a new reference
const selectTodoDescriptions = (state) => {
// This creates a new array reference!
return state.todos.map((todo) => todo.text);
};