Redux basics
Basics of app state management with Redux
- Redux is a pattern and library for managing and updating application state, using events called "actions"
- when used in conjunction with React, it has the practical effects of :
- decoupling the application's state from the individual component's states
- centralizing the application state management in a single javascript object called a store
- Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur
✔️ Note : NOT ALL APPS NEED REDUX
- using Redux becomes an option when :
- multiple components from different parts of the application need to share the same state
- enabling the sharing of state between components simply by lifting the state up accordingly becomes too complex or unfeasible
- immutability : Redux expects that all state updates are done immutably
-
actions : an object representing the need for a change in the application's state, with the following properties :
- type : description of the action (ex
user/hasLoggedIn
) - payload : information relevant to the action (ex
{date: new Date(), token: `xxxxx.yyyyy.zzzzz`}
)
- type : description of the action (ex
- action creator : function that accepts the variadic values of an action as parameters and returns a new action (ie. type is not variadic for a given action)
- reducer : function that accepts the current state and an action as parameters and returns the next state (kind of an event listener for actions)
const reducer = (state, action) => {
// 1. implements conditional logic to decide wether the action is relevant to the current reducer
// 2. if so, create a copy of the state and update it
// 3. return the new state
return newState;
};
✔️ Note : reducers get their name from the Array.reduce()
callback, they are similar in the sense that they return a new object without mutating the original object
-
store : plain javascript object holding the application's state :
- the store is created by passing a reducer to the function configureStore
const store = configureStore({ reducer: sampleReducer });
- the store implements the following methods :
- ```store.getState()``` : returns the current application's state
- ```store.dispatch(action)``` : calls the reducer with the current state and the ```action``` passed as parameter
- selectors : functions that extract specific parts of the state by returning properties from the current state value
-
newState
should be calculated using onlystate
andaction
parameters - the
state
parameter must not be modified but immutably updated by copying the existingstate
and changing the copy - neither asynchronous logic nor anything that could cause "side effects" should be implemeted in it (reducers should be pure functions)
- THE PATTERN FOR IMMUTABLE UPDATE OF THE STATE IS AS FOLLOWS :
return {
// use ES2018 object spread to copy all the state properties
// into a new object without mutating the state object
...state,
// perform an immutable update by adding new properties or
// accessing the existing keys to update existing properties
value: 123
}
- THE MOST COMMON MISTAKE IN REDUX IS ACCIDENTALLY MUTATING THE STATE IN A REDUCER
-
create-react-app template for redux :
- scaffold a react project with a barebones redux application structure
-
redux toolkit :
- provides the primitives for store configuration and slice configuration
-
react-redux
- provides additional react hooks to connect react components to the redux store :
useSelector()
,useDispatch()
- provides additional react hooks to connect react components to the redux store :
- each application feature implements its own reducer function to interact with the redux store
- the reducer for a feature is usually exported by a redux slice (see below)
- during initialization, the
configureStore
primitive is passed an object containing all of the above reducers :
const store = configureStore({
reducer: {
// feature 1 : imported reducer function in charge of updating state.userSession
userSession: userSessionReducer,
// feature 2 : imported reducer function in charge of updating state.userData
userData: userDataReducer,
}
});
- a redux slice is a redux-toolkit primitive containing all the state logic (reducers and actions) for a given feature :
- it accepts an initial state for the application and a collection of reducers
- it returns multiple action creator functions derived from the reducers
- it returns a global reducer function used to configure the store
- during initialization, the
createSlice
primitive is passed an object containing :
// perform async request to server to provide credentials and retrieve a session token ...
const login = createAsyncThunk(
`userSession/login`,
async credentials => {
// async authentication request
const response = await logUserToPlatform(credentials);
// the returned value becomes the `fulfilled` action payload
return response.data;
}
);
const slice = createSlice({
// "state slice" name
name: `userSession`,
// initial state for the slice
initialState: {
loggedIn: false,
lastLoginDate: new Date(`<last login date from db>`),
sessionToken: null
},
// reducers for the slice (create reducers and export action creator functions)
reducers: {
logout: state => {
// we assess that this slice manages a stateless client-side session using a jwt ...
state.loggedIn = false;
state.sessionToken = null;
}
},
// external reducers (import actions and create reducers for the slice)
extraReducers: builder => {
builder
// the builder cases mimic the possible promise states
.addCase(login.pending, state => {
// nothing to update there ...
})
.addCase(login.fulfilled, (state, action) => {
// update the state at this stage
state.loggedIn = true;
state.lastLoginDate = new Date();
state.sessionToken = action.payload.jsonWebToken;
})
.addCase(login.rejected, (state, action) => {
// update the state at this stage
state.loggedIn = false;
state.sessionToken = null;
});
}
});
const
// retrieve the action creator function from the slice reducers
// action type: userSession/logout
{logout} = slice.actions,
// retrieve the global reducer function for the slice
reducer = slice.reducer;
// action creators and slice reducer are now available for export.
export {login, logout, reducer}
- redux-toolkit primitives
createSlice
andcreateReducer
are the only places in which reducers can mutate the state - the actions exported from the slice will immutably update the state when executed no matter what (through immer)
- as a consequence, redux-toolkit powered reducers do not need to return the new state (since they can mutate the existing state)
- a thunk is a special kind of redux function that allows the execution of asynchronous code
- they are basically async wrapper functions around standard redux actions that accept
store.dispatch
as a parameter :
export const sampleThunkCreator = url => async dispatch => {
const
// trigger an async request
result = await fetch(url);
// dispatch the result dependent action to the store
dispatch(basicReduxActionCreator(await readable.json()));
}
// then, use with the standard dispatch method
store.dispatch(sampleThunkCreator(`http://somedata.com/get`))
- thunks are not part of the core redux library by default but are made available through the
redux-thunk
plugin - however, the redux-toolkit
configureStore
primitive embarks all the necessary setup to support thunks
- the redux store cannot be imported into components files, thus redux exposes some cutom hooks to allow components to interact with the store
-
useSelector
: use a selector function to access a specific part of the state -
useDispatch
: dispatch an action to the store from a component - the redux custom hooks connect the react components to the store through the use of the
<Provider>
component that wrapps the react app
✔️ Note : STATE THAT NEEDS NOT TO BE SHARED GLOBALLY SHOULD REMAIN AT COMPONENT LEVEL AND NOT GO IN THE STORE