Complicating Things And Completing The Workshop: Advanced React Patterns From Epic React by Kent C. Dodds

completed the Advanced React Patterns workshop!

I finally finished the Advanced React Patterns workshop!
It was so complicated (info overload) but I am happy I was able to finally complete this!
Can I say I'm good at React now? I'm not sure, but this is the start of me being aware of what's happening in the code and to apply what I've learned from the workshop especially the different React Patterns.

Course Overview

image.png

The exercises aren't that hard but it wasn't easy finishing up the lesson. Why? Because this is about React Patterns, you need to fully understand what's it about. And for me, usually I'm also thinking how I'll be able to apply the lesson to my project at work.

Also, you need to fully understand how context and reducer works because there's a lot of it in this course. I don't usually use these so it took me a while to fully grasp them.

Learnings

Context Module Functions

Not really the name of the pattern, just a made up name by Kent C. Dodds, (or maybe this is already its official name?). According to him, it's also the same pattern used by Facebook.

One liner: The Context Module Functions Pattern allows you to encapsulate a complex set of state changes into a utility function which can be tree-shaken and lazily loaded.

source: Epic React workshop

The important skill here that you need to have is knowing context and reducer. Why? Because you'll be using both of them in this pattern.

Screenshot 2024-04-08 at 20.35.43.png

This is how my App is implemented. It looks so clean and simple, right?
In my previous post, I've mentioned about context and reducers so in this code here, I created a UserContext and it will be used by UserSettings and UserDataDisplay.

function UserProvider({children}) {
  ...
  const [state, dispatch] = React.useReducer(userReducer, {
    status: null,
    error: null,
    storedUser: user,
    user,
  })
  const value = [state, dispatch]
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}

function useUser() {
  const context = React.useContext(UserContext)
  if (context === undefined) {
    throw new Error(`useUser must be used within a UserProvider`)
  }
  return context
}

function updateUser(dispatch, user, updates) {
  dispatch({type: 'start update', updates})
  return userClient.updateUser(user, updates).then(
    updatedUser => dispatch({type: 'finish update', updatedUser}),
    error => dispatch({type: 'fail update', error}),
  )
}
// in another file

function UserSettings() {
  const [{user, status, error}, userDispatch] = useUser()
  ...  
  function handleSubmit() {
    updateUser(userDispatch, user, formState)
  }
  ...
}

function UserDataDisplay() {
  const [{user}] = useUser()
  return <pre>{JSON.stringify(user, null, 2)}</pre>
}

The userReducer() manages the user information, and this reducer is used in the UserProvider. This provider will then return both the state and the dispatch function as context. The useUser() function consumes this context, then the UserSettings() consumes that.

Now let's focus on updateUser() where this was the main goal of the exercise in this lesson.

It would have been okay to not make this function, and instead leave this code in handleSubmit() inside UserSettings() but we don't want that. If we leave it to the consumer (which in this case is UserSettings), we might be missing something. So instead of doing that in the consumer, we create this function in the context. We also need to pass dispatch as props to this function.

In the event where there will be a lot of consumers, they can use this helper function which is our context module function.

Compound Components

Probably my favorite pattern out of everything I've learned in the course.

One liner: The Compound Components Pattern enables you to provide a set of components that implicitly share state for a simple yet powerful declarative API for reusable components.

source: Epic React workshop

An example of a compound component

Toggle, our parent component, will create a copy of the children components. ToggleOn and ToggleOff components take a text as its children - which will display based on the state variable on. Where is on declared? In our parent component. We're also passing ToggleButton component which will call a Switch component that needs on and onClick.

image.png

Notice that in the App, we're not passing around the variables but instead it's just a very simple declaration of the components. The main implementation is happening in the parent component Toggle where the needed variables and functions are declared.

This will make the code cleaner. I always like not to overcomplicate components. The short and concise, the better. If the components stick to their use case, it's easier to reuse them.

Flexible Compound Components

The only issue with the Compound Components is when children are nested like in this App function.

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <div>
          <ToggleButton />
        </div>
      </Toggle>
    </div>
  )
}

To make this work, context is needed to be shared among the children. So in Toggle, instead of cloning the children, we expose on and toggle in the context provider.

const ToggleContext = React.createContext()

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  const value = [on, toggle]
  return <ToggleContext.Provider value={value}>{children}</ToggleContext.Provider>
}

function useToggle() {
  const context = React.useContext(ToggleContext)
  if (!context) {
    throw new Error(`useToggle must be used within a Toggle`)
  }
  return context
}

ToggleContext is then consumed in useToggle() so the children components will just have to call this function.

function ToggleOn({children}) {
  const [on] = useToggle()
  return on ? children : null
}

function ToggleOff({children}) {
  const [on] = useToggle()
  return on ? null : children
}

function ToggleButton({...props}) {
  const [on, toggle] = useToggle()
  return <Switch on={on} onClick={toggle} {...props} />
}

Prop Collections and Getters

There are instances that the props to a component are reusable. This pattern will come in handy.

function useToggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  const getTogglerProps = ({onClick, ...props}) => {
    return {
      'aria-pressed': on,
      onClick: () => {
        onClick && onClick()
        toggle()
      },
      ...props,
    }
  }

  return {on, toggle, getTogglerProps}
}

function App() {
  const {on, getTogglerProps} = useToggle()
  return (
    <div>
      <Switch {...getTogglerProps({on})} />
      <hr />
      <button
        {...getTogglerProps({
          'aria-label': 'custom-button',
          onClick: () => console.info('onButtonClick'),
          id: 'custom-button-id',
        })}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

The useToggle custom hook will prepare the default props, and because it also accepts the rest of the ...props (by using spread operator), the default props can be overridden.

This is usually how I code, except I don't use custom hooks for it. Next time, I'll try to make use of it.

Looping and Calling Functions

Note: Not a react pattern.

I also learned of this cool way of calling all functions instead of the one I made.

My code:

onClick: () => {
  onClick && onClick()
  toggle()
},

A better way:

onClick: callAll(onClick, toggle),

...
function callAll(...fns) {
  return (...args) => {
    fns.forEach(fn => {
      fn && fn(args)
    });
  }
}

It loops through all the props and calls them, with args or not. So now, you don't need to bother having every line call the function. Just pass all the functions in callAll and it will do the function calls.

State Reducer

One liner: The State Reducer Pattern inverts control over the state management of your hook and/or component to the developer using it so they can control the state changes that happen when dispatching events.

source: Epic React workshop

For this pattern, a custom hook is needed. In that custom hook, you need to create a reducer. The main functions are here so your component is simpler and cleaner.

Screenshot 2024-04-09 at 2.54.00.png

So now, in App, you just need to call the custom hook. It accepts a reducer too if you have one, but if there's none declared, the custom hook will use the default reducer.

function App() {
  ...
  function toggleStateReducer(state, action) {
    if (action.type === Actions.Toggle && clickedTooMuch) {
      return {on: state.on}
    }
    return toggleReducer(state, action)
  }

  const {on, getTogglerProps, getResetterProps} = useToggle({
    reducer: toggleStateReducer,
  })

  ...
}

You can also read more about this pattern in Kent's blog post.

Conclusion

What's cooler is that these patterns are being used in real world projects. So probably when I see the code or work in those projects, it's not going to be alien to me.

There are probably more React patterns out there but I'm already okay with what I've learned in this course. It's already a lot to take in. Besides, I still have to decide what pattern suits best to my project.

Actually in my current task right now for work, I've started using the State Reducer pattern. It's not easy to make it, especially when I just learned of this pattern a few days ago. It's not so perfect yet but I managed to make it work.

So maybe like me, you'll be able to get some tips and tricks that you can use for your code, then that would be very awesome!

This concludes the workshop for Epic React Standard plan. I learned a lot and hopefully you did too.


Thanks for reading!
See you around! じゃあ、またね!


All photos are screenshots from my code unless stated otherwise.

Sort:  

Congratulations @wittythedev! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)

You received more than 900 upvotes.
Your next target is to reach 1000 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Check out our last posts:

Feedback from the April Hive Power Up Day
Hive Power Up Month Challenge - March 2024 Winners List