Redux basics

Basics of app state management with Redux

view on github

What is 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

Why use redux ?

  • using Redux becomes an option when :
    1. multiple components from different parts of the application need to share the same state
    2. enabling the sharing of state between components simply by lifting the state up accordingly becomes too complex or unfeasible

Core concepts

  1. immutability : Redux expects that all state updates are done immutably
  2. 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`})
  3. 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)
  4. 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

  1. 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 
  1. selectors : functions that extract specific parts of the state by returning properties from the current state value

Rules when writing reducers

  • newState should be calculated using only state and action parameters
  • the state parameter must not be modified but immutably updated by copying the existing state 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

React / redux essentials

  • 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()

Store initialization

  • 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,
    } 
});

Redux slices initialization

  • a redux slice is a redux-toolkit primitive containing all the state logic (reducers and actions) for a given feature :
    1. it accepts an initial state for the application and a collection of reducers
    2. it returns multiple action creator functions derived from the reducers
    3. 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 and createReducer 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)

Thunks

  • 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

Redux custom hooks

  • 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