Adding Redux and advanced React Router - vonschappler/Ultimate-React GitHub Wiki

Modeling the User state with Redux

Until this moment, in every part of the application which required some piece of state, that was mainly hardcoded, as part of the project development (step 2 of planning a React Application) discussed here. This is the time where we'll start the process of reming all hard-coded "states" and converting them into real use-case by converting the fake state

So, going back to the application, it's time to use Redux, since it was the technology chosem to manage global state, starting by the first global state, the user state, in order to manage the simple user feature, where the user name and some data, such as phone number and address are the requirements from where a user can order an pizza. Since Redux and global state management were already discussed, only some additional notes will be placed here, IF necessary.

Additional notes:
  1. Because we want still to be able to edit the user name on the page order/new, instead of making use of the prop value inside the customer input component, we make use of the defaultValue prop, as displayed below:

    // file CreateOrder.jsx
    
    // imports and global components definitions
    
    function CreateOrder() {
      // react/javascript logic
    
      return (
        {/* some jsx code */}
        <input
          type="text"
          name="customer"
          required
          className="input grow"
          defaultValue={username}
        />
        {/* some more jsx code */}
      )
    }
    
    export default CreateOrder;

Modeling the Cart state with Redux

In a similar way as done with the user state, it's now time to work with the cart state. So we'll keep using Redux to manage this state and add some extra functionalities, such as add new items to the cart, removing items from the cart and enabling the use of the cart for the creation of new orders, as well as connecting all this with the Cart Overview component, so both components are synched to the same state.

Additional notes:
  1. When deriving states in Redux, the best practices suggests that this derived state is calculated inside the selector function of a state, as shown below, where we get the current cart and get the total items quantity on it:

    // file CartOverview.jsx
    
    // imports and global components definitions
    
    function CartOverview() {
      // react/javascript logic
    
      const totalCartQuantity = useSelector((state) =>
        state.cart.cart.reduce((sum, item) => sum + item.quantity, 0)
      );
    
      // more react/javascript logic
    
      return <div>{/* some jsx code*/}</div>;
    }
    
    export default CartOverview;
  2. When working with those selectors it's still a better practice to move the selector function to the slice, which would then change the cartSlice and CartOverview to something similar to:

    // cartSlice.js
    
    // slice definitions here
    
    // it's still part of those best practices that any selector function added inside the a slice starts with the word "get",
    //so we can easly understand why that selector function was created.
    export const getTotalCartQuantity = (state) =>
      state.cart.cart.reduce((sum, item) => sum + item.quantity, 0);
    // file CartOverview.jsx
    
    // imports and global components definitions
    import { getTotalCartQuantity } from 'path/to/cartSlice';
    
    function CartOverview() {
      // react/javascript logic
    
      const totalCartQuantity = useSelector(getTotalCartQuantity);
    
      // more react/javascript logic
      return <div>{/* some jsx code */}</div>;
    }
    
    export default CartOverview;
  3. Keep in mind that making use of this method in large applications may cause some peformance issues, which can be solved by the user of the reselect library. Since this is not the case with this application, this library won't be covered.

  4. It's possible to reuse any reducer defined by a slice, by making use of sliceName.caseReducers.reducerName(state, action), just as shown in the snippet of code below, where a pizza is removed from the cart when its quantity reaches 0:

    // cartSlice.js
    
    // slice definitions here
    
    const cartSlice = createSlice({
      name: 'cart',
      initialState,
      reducers: {
        // other reducers
        deleteItem(state, action) {
          state.cart = state.cart.filter(
            (item) => item.pizzaId !== action.payload
          );
        },
        decreaseItemQuantity(state, action) {
          const item = state.cart.find(
            (item) => item.pizzaId === action.payload
          );
          item.quantity--;
          item.totalPrice = item.quantity * item.unitPrice;
          if (item.quantity === 0)
            cartSlice.caseReducers.deleteItem(state, action);
        },
        // some more reducers
      },
    });
    
    // some more slice definitions here
  5. In order to clear the cart after placing an order, it's possible to directly import the store and dispatch the action clearCart inside the action function defined inside CreateOrder.jsx. The example below displays how to make use of this technique:

    // react imports
    import { clearCart, getCart } from 'path/to/cartSlice';
    import store from 'path/to/store';
    
    // functional component definitons and export
    
    export async function action({ request }) {
      // action definitions and validation
    
      const newOrder = await createOrder(order);
    
      store.dispatch(clearCart());
    
      return redirect(`/order/${newOrder.id}`);
    }

    This technique should not be overused, because this can break some of the code optmizations done by Redux.

  6. Even though in this course we already talked about Thunks it's time to work with some more advanced features of it. This is where createAsyncThunk comes into place and the way of using it can be found on the snippet of code below:

    // file userSlice.js
    
    // react imports
    import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
    
    // thunk created with createAsyncThunk
    export const fetchAddress = createAsyncThunk(
      'user/fetchAddress',
      async function () {
        // function to handle the address fetching
        // this should return the payload to be passed to the state
        // in this case we are interessed in the position and address
        return { position, address };
      }
    );
    
    const initialState = {
      username: '',
      status: 'idle',
      position: {},
      address: '',
      error: '',
    };
    
    const userSlice = createSlice({
      name: 'user',
      initialState,
      reducers: {
        // usual reducers
      },
      // redux builder definitions to connect the created thunk with the reducers
      extraReducers: (builder) =>
        builder
          .addCase(fetchAddress.pending, (state, action) => {
            state.status = 'loading';
          })
          .addCase(fetchAddress.fulfilled, (state, action) => {
            state.status = 'idle';
            state.position = action.payload.position;
            state.address = action.payload.address;
          })
          .addCase(fetchAddress.rejected, (state, action) => {
            state.status = 'error';
            state.error = action.error.message;
          }),
    });
    
    // other exports/definitions for the slice

    NOTES:

    1. The createAsyncThunk provided by Redux receives as arguments a custom action type (as a string) and an async function.
    2. Remember that thunks is a middleware function. It means it'll run after the dispatch, but before applying the state changes.
    3. createAsyncThunk is part of Modern React Toolkit. This will create three additional action types, one for the pending state, one for the fulfilled state and one for the rejected state, which need to be handled individually inside the reducers, as seen above in the extraReducers property.
  7. It's possible to load data without causing navigation by using some of the advanced ReactRouter new features (6.4+).

  8. It's also possible to implement data writing without causing a new nagivation in a similar way as we fetch data without navigation. The final code with the application of these techniques can be found here.

    NOTES:

    1. For updating data without nagivation, it's required that we make use of the component <fetcher.Form> which contains inputs, buttons and everything else required for each case.
    2. Writing data will force a revalidation of the data, which means that React becomes aware that the data has changed as a result of the action "update". This will cause a data re-fetch and a page re-render with the new data.
⚠️ **GitHub.com Fallback** ⚠️