Skip to content

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.

Notifications You must be signed in to change notification settings

gorakhjoshi/ReactPatternContextModuleFunctions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Context Module Functions

📝 Your Notes

Background

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.

Let's take a look at an example of a simple context and a reducer combo:

const CounterContext = React.createContext();

function CounterProvider({ step = 1, initialCount = 0, ...props }) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step;
      switch (action.type) {
        case 'increment': {
          return { ...state, count: state.count + change };
        }
        case 'decrement': {
          return { ...state, count: state.count - change };
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`);
        }
      }
    },
    { count: initialCount }
  );

  const value = [state, dispatch];
  return <CounterContext.Provider value={value} {...props} />;
}

function useCounter() {
  const context = React.useContext(CounterContext);
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`);
  }
  return context;
}

export { CounterProvider, useCounter };
// src/screens/counter.js
import { useCounter } from 'context/counter';

function Counter() {
  const [state, dispatch] = useCounter();
  const increment = () => dispatch({ type: 'increment' });
  const decrement = () => dispatch({ type: 'decrement' });
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}
// src/index.js
import { CounterProvider } from 'context/counter';

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

I want to focus in on the user of our reducer (the Counter component). Notice that they have to create their own increment and decrement functions which call dispatch. I don't think that's a super great API. It becomes even more of an annoyance when you have a sequence of dispatch functions that need to be called (like you'll see in our exercise).

The first inclination is to create "helper" functions and include them in the context. Let's do that. You'll notice that we have to put it in React.useCallback so we can list our "helper" functions in dependency lists):

const increment = React.useCallback(
  () => dispatch({ type: 'increment' }),
  [dispatch]
);
const decrement = React.useCallback(
  () => dispatch({ type: 'decrement' }),
  [dispatch]
);
const value = { state, increment, decrement };
return <CounterContext.Provider value={value} {...props} />;

// now users can consume it like this:

const { state, increment, decrement } = useCounter();

This isn't a bad solution necessarily. But as my friend Dan says:

Helper methods are object junk that we need to recreate and compare for no purpose other than superficially nicer looking syntax.

What Dan recommends (and what Facebook does) is pass dispatch as we had originally. And to solve the annoyance we were trying to solve in the first place, they use importable "helpers" that accept dispatch. Let's take a look at how that would look:

// src/context/counter.js
const CounterContext = React.createContext();

function CounterProvider({ step = 1, initialCount = 0, ...props }) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step;
      switch (action.type) {
        case 'increment': {
          return { ...state, count: state.count + change };
        }
        case 'decrement': {
          return { ...state, count: state.count - change };
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`);
        }
      }
    },
    { count: initialCount }
  );

  const value = [state, dispatch];

  return <CounterContext.Provider value={value} {...props} />;
}

function useCounter() {
  const context = React.useContext(CounterContext);
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`);
  }
  return context;
}

const increment = (dispatch) => dispatch({ type: 'increment' });
const decrement = (dispatch) => dispatch({ type: 'decrement' });

export { CounterProvider, useCounter, increment, decrement };
// src/screens/counter.js
import { useCounter, increment, decrement } from 'context/counter';

function Counter() {
  const [state, dispatch] = useCounter();
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={() => decrement(dispatch)}>-</button>
      <button onClick={() => increment(dispatch)}>+</button>
    </div>
  );
}

This may look like overkill, and it is. However, in some situations this pattern can not only help you reduce duplication, but it also helps improve performance and helps you avoid mistakes in dependency lists.

I wouldn't recommend this all the time, but sometimes it can be a help!

📜 If you need to review the context API, here are the docs:

🦉 Tip: You may notice that the context provider/consumers in React DevTools just display as Context.Provider and Context.Consumer. That doesn't do a good job differentiating itself from other contexts that may be in your app. Luckily, you can set the context displayName and it'll display that name for the Provider and Consumer. Hopefully in the future this will happen automatically (learn more).

const MyContext = React.createContext();
MyContext.displayName = 'MyContext';

Exercise

👨‍💼 We have a user settings page where we render a form for the user's information. We'll be storing the user's information in context and we'll follow some patterns for exposing ways to keep that context updated as well as interacting with the backend.

💰 In this exercise, if you enter the text "fail" in the tagline or biography input, then the "backend" will reject the promise so you can test the error case.

Right now the UserSettings form is calling userDispatch directly. Your job is to move that to a module-level "helper" function that accepts dispatch as well as the rest of the information that's needed to execute the sequence of dispatches.

🦉 To keep things simple we're leaving everything in one file, but normally you'll put the context in a separate module.

About

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.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published