RFR Section 6 – Leveling Up The Way We Approach State


React

Updated Mar 14th, 2022

Table of Contents

Course Summary

Chapter Details

Chapter 36. Context

Context let’s us pass data down in a more elegant fashion. Want to consume state/data from many components. For some apps you may need to pass state down 3 or 4 or 5 layers deep. With context we wrap entire bit of JSX within one overall parent or container component and then every component nested inside can directly consume no matter how many layers deep it is nested.

Create new “ExampleContext.js” file in the app folder.

import {createContext} from 'react'

const ExampleContext = createContext()

export default ExampleContext

Import this new file into the “Main.js” file and then wrap all components in “<ExampleContext.Provider>” tags and on the opening tag pass a “value” prop of “{addFlashMessage}” and on the “CreatePost” component tag get rid of the existing “addFlashMessage” prop.

// in Main.js file

import ExampleContext from ExampleContext

// in the JSX area...

<ExampleContext.Provider value={addFlashMessage}>

 // all other components go here

</ExampleContext.Provider>

In “CreatePost.js” file, or any component we want to access or consume the context, import the file and give yourself easy access to, “useContext” from ‘react’ and in component function set a constant variable “addFlashMessage” set to “useContext().”

Import the “ExampleContext” file and now in the “useContext()” pass it “ExampleContext.” Now we have access!

// in CreatePost.js

import {useContext} from 'react'

import ExampleContext from ExampleContext

const addFlashMessage = useContext(ExampleContext) // tells it which context to use

So in the JSX we can leverage “addFlashMessage” without the “props.” prefix

This may not seem impressive now but the reason context is so useful is that we can leverage this from any component in the nested component tree.

In the “Main.js” file on the “ExampleContext.Provider” we can set “value” to more than one thing

value={{addFlashMessage, SetLoggedIn}}

Since we have multiple items in “value,” back in the “CreatePost.js” file we need to wrap addFlashMessage in curly brackets to destructure it.

const {addFlashMessage} = useContext(ExampleContext)

In “Header.js” we can get rid of the manually passed down prop “setLoggedIn”

{props.loggedIn ? <HeaderLoggedIn setLoggedIn={props.setLoggedIn}/> : <HeaderLoggedOut setLoggedIn={props.setLoggedIn} />

// becomes

{props.loggedIn ? <HeaderLoggedIn/> : <HeaderLoggedOut/>

// And in these files just use Context

In “HeaderLoggedIn.js” we can gain access to “useContext” and import in the “ExampleContext.js” file and create a constant variable of “{setLoggedIn}” that is set to “useContext(ExampleContext).” Now we can remove “props.” in front of the “setLoggedIn(false)” line.

In “HeaderLoggedOut.js” we do the same thing.

Chapter 37. useReducer

An alternative to “useState” is “useReducer.” The official React docs say you should use “useReducer” over “useState” when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

See “useReducer” in action: In the “Main.js” file import {useReducer} from “react.”

// in Main.js file

import {useReducer} from 'react'

// inside of component function

// the ourReducer function and initialState will be defined here

const [state, dispatch] = useReducer(ourReducer, initialState)

“useReducer” is similar to “useState” in that it returns an array with two things: a piece of state, and something you can use to update state, a dispatch.

Instead of a simple function to update state, this dispatch, is a tool we can use to “dispatch” actions. You say what you want to do, but you don’t say how it actually gets done, that’s where our “ourReducer” function comes into play.

So “useReducer()” gets two arguments: 1.) the name of the reducer function you will create and 2.) an initial state.

examples of calling dispatch

dispatch({type: "login"})
dispatch({type: "logout"})
dispatch({type: "flashMessage", value: "Congrats, you created a post."})

The reducer function is where you actually say how these things happen or how the state data of our application should change for these particular actions. The reducer function gets two parameters, “state” and “action.” The idea here is any time you call dispatch, whatever you include in the parentheses when you call dispatch, that is what gets passed to the reducer function as the “action.”

We wouldn’t call “dispatch” in the “Main.js” file, instead pass “dispatch” with context down to children components and we call there and the “ourReducer” function handles what actually happens.

In the reducer function the convention is to use a switch statement.

To focus on the “ourReducer” function:

function ourReducer (state, action) {
  switch (action.type) {
    case "login":
      return x // this is the new state object we return
    case "logout":
      return x
    case "flashMessage":
      return x
  }
}

To focus on the “initialState” lets set this to an object above the “ourReducer” function:

const initialState = {
  loggedIn: Boolean(localStorage.getItem("complexappToken")),
  flashMessages: []
}

It’s important that, in React, we don’t want to modify or mutate state. So we create a new object based on the previous one.

function ourReducer (state, action) {
  switch (action.type) {
    case "login":
      return {loggedIn: true, flashMessages: state.flashMessage}
    case "logout":
      return {loggedIn: false, flashMessages: state.flashMessage}
    case "flashMessage":
      return {loggedIn: state.loggedIn, flashMessages: state.flashMessage.concat(action.value)}
  }
}

So what is “useReducer” really doing? When we call it we give it our initial state and a function, and in return, it gives us two things we chose to name “state” and “dispatch.” And now we have just two things that can power our entire application. Very powerful.

Chapter 38. A Powerful Duo: useReducer & Context

Our goal for this lesson, if you jump into “Main.js,” is to delete or remove the old way of managing state for logged in and flash messages that was using “useState.”

On the “ExampleContext.Provider,” hollow out the “value” prop and instead have, within a double pair of curly brackets, we want state, dispatch.

<ExampleCOntext.Provider value={{state, dispatch}}>

We need to pause here because although this will work it is not optimal from a performance standpoint. You need to acknowledge that anytime anything in this object changes, any components that are consuming the content will re-render to make sure they have this latest value, and some of our components will not need access to our “global state.” Some components will only need access to our “dispatch” and we don’t want those components unnecessarily re-rendering every time “global state” changes.

The way that the React team recommends we set this up is to have two context providers in our JSX, one context provider for “state,” and one context provider for “dispatch.” This way it will be up to each individual component to decide which context they want to consume and watch for changes

So we wrap our JSX in “<StateContext.Provider>” tags and “<DispatchContext.Provider>” tags. Set the “value” for the “<StateContext.Provider>” to “state” and the “value” for the “<DispatchContext.Provider>” to “dispatch”

<StateContext.Provider value={state}>
  <DispatchContext.Provider value={dispatch}>

     "...other components here"
  
  </DispatchContext.Provider>
</StateContext.Provider>

We need to create separate context files, in our “app” folder, with matching names. We can copy and paste from the “ExampleContext.js” file.

// StateContext.js file
// Do the same to create a DispatchContext.js file

import { createContext } from "react"

const StateContext = createContext()

export default StateContext

We no longer need our “ExampleContext.sj” file so we can delete that and let’s go into “Main.js” file and import these new context files.

// in Main.js file

import StateContext from "./StateContext"
import DispatchContext from "./DispatchContext"

We have now made “state” and “dispatch” available from any of the components within our application and now we just need to go into the individual components to make them leverage the “state” and “dispatch.”

We no longer have a variable named flash messages as we now have a property named flash messages, so on the “<FlashMessages />” component, we need to change the “messages” prop to be “state.flashMessages.”

Let’s begin working on our “<Header />”component tag. We no longer need to be passing in “loggedIn” so remove this so it’s just a blank tag.

In “Header.js” file, instead of looking in “props.loggedIn” we can now just look at that from our global state.

At the very top bring in “useContext” from “react,” and just inside the header component function set a constant variable named “appState” and set it equal to “useContext,” (This could be global state or any other term you want to use). Be sure to import in the “StateContext” file. Now in the JSX, instead of “props.loggedIn” it would be “appState.loggedIn.”

Now in “HeaderLoggedIn.js” and “HeaderLoggedOut.js” files bring in “DispatchContext” for the flash messages and update the “setLoggedIn” line.

cont appDispatch = useContext(DispatchContext)

function handleLogout() {
  appDispatch({type: 'logout'}) // this would be 'login' in HeaedrLoggedOut.js
}

In “Create-Post.js” file go through a similar step. Bring in DispatchContext and set up appDispatch

appDispatch({type: })

Forgot that back in “Main.js” file, in the “Route” for the homepage, “/,” we need to make “loggedIn” be “state.loggedIn,” since we are in the same component this was created in we don’t need to worry about context.

So why would you bring in “StateContext” versus “DispatchContext” or vice-versa? If you are consuming existing state, (to conditionally show components for example), you use “StateContext.” If you are dispatching actions, to login, logout, or show a flash message for example, you use “DispathContext.” If a component needs both import and use both, if it only needs one then only import the one.

Chapter 39. What is Immer?

Not part of official core React, but allows you to write immutable code in an mutable fashion, (or vice versa).

The problem “immer” solves: For each type of action that gets dispatched, we want to update the state of our app in a different way. Cannot just change the one thing we want, we need to spell it out for everything. May have 39 other properties you don’t want to change or repeat.

“Immer” gives us a “draft” or carbon copy. “Immer” is not an obscure package, 3.2m weekly downloads. You need to “npm install immer” and “use-immer” (to use with React). In the “Main.js” file, import {useImmerReducer} from “useImmer.”

Change “useReducer” to “useImmerReducer” and then in the “ourReducer” function change “(state, action)” to be “(draft, action).”

Now we can use “.push” instead of “.concat,” since we can now directly modify or mutate the array thanks to “immer.”

function ourReducer (draft, action) {
  switch (action.type) {
    case "login":
      draft.loggedIn = true
      return // can also use break
    case "logout":
      draft.loggedIn = false
      return // can also use break
    case "flashMessage":
      draft.flashMessages.push(action.value)
      return // can also use break
  }
}

Chapter 40. useEffect Practice

The problem we want to solve is we are storing some data the server gives us in local storage and we are working with this same piece of data in many locations, (setting items, getting items, etc.). This the number one source of bugs as your app grows in complexity.

Any sort of local storage work should happen in the “Main.js” file, (Get and set the user data from the server in local storage).

In the “Main.js” file add a “user” object in the “initialState” object to “get” the data from local storage.

user: {
   token: localStorage.getItem("complexappToken")
   username: localStorage.getItem("complecappUsername")
   avatar: localStorage.getItem("complexappAvatar")
}

In order to “set” the data into local storage in the “Main.js” file, we need to send the data from the “HeaderLoggedOut.js” file to the “Main.js” file, via by dispatching an action.

if (response.data) {
  appDispatch({type: "login", data: response.data})
}

Note that the “data” property above could be named anything you want.

Now we can work with this data in “Main.js” by adding to the “login” case in the “ourReducer” switch function.

case "login":
  draft.loggedIn = true
  draft.user = action.data
  return

Now we can are pulling existing user data from local storage and setting as state.

Now we need to store into local storage after a successful login and remove from local storage after a logout, and we will also perform that in “Main.js,” with the help of “useEffect,” (since this should not happen in the reducer function.)

// in main.js under the const [state, dispatch] line

useEffect(()=>{
  if (state.loggedIn) {
    localStorage.setItem("complexappToken", state.user.token)
    localStorage.setItem("complexappUsername", state.user.username)
    localStorage.setItem("complexappAvatar", state.user.avatar)
  } else {
    localStorage.removeItem("complexappToken")
    localStorage.removeItem("complexappUsername")
    localStorage.removeItem("complexappAvatar")
  }
}, [state.loggedIn])

// return jsx here

We also need to go to the “HeaderLoggedIn.js” file and remove the local storage lines in the “handleLogout” function.

Make sure where the application needs the avatar, token, and username, this is pulling from state instead of local storage.

Bring in StateContext in “HeaderLoggedIn,” and updated the “src” attribute for the avatar image.

In the “Home.js” file bring in “StateContext” and inside the strong tag where you have your username bring in “appState.user.username.”

In “CreatePost.js” file, bring in “StateContext” and, in the “axios” request, update the “token” value to “appstate.user.token.”

Log in to Atlas and ensure the post was created with the token from state.