Skip to content

Latest commit

 

History

History
352 lines (265 loc) · 10.8 KB

README.md

File metadata and controls

352 lines (265 loc) · 10.8 KB

React Subscribe Context

A consistent way of state management to avoid prop drilling, and is optimized for component rerenders.

And it's IE11 compatible!

Introduction

React Context is an amazing tool to help avoid prop drilling, but it comes with a price. It's not a pleasant dev experience to create setters for every value you want in your context, and each time those values change, it'll rerender every child it holds, unless they're memoized, but who wants to put in that extra work to memoize everything? And even if they're memoized, if they're consumers, they'll always get rerendered.

I was inspired by react-hook-form and their useWatch, which subscribed to only changes to a specific value of a form. I loved that feature and thought it'd be great if React Context could do that too. Then I learned about react-tracked. It did exactly what I was looking for, except that it wasn't IE11 compatible, so I decided to create react-subscribe-context.

Using Proxy and EventEmitter, I created a tool where you can subscribe to a value just by accessing it, and it works for nested objects as well. It's simple to set up and works similar to the React hook, useState!

Rendering performance example

With render highlighting on via React Developer Tools, the example below shows how rendering only happens to components with data that had changed.

react-subscriber-render-performance

Installation

  npm install react-subscribe-context
  yarn add react-subscribe-context

Usage/Examples

Setup

// SpiderManContext.ts
import { createSubscriberContext } from "react-subscribe-context";

const initialState = {
  user: {
    name: {
      first: "Peter",
      last: "Parker",
    },
  },
  movieCounter: 9,
};

export const {
  Context: SpiderManContext,
  Provider: SpiderManProvider, // Note: This is not the same as what Context.Provider returns
} = createSubscriberContext({ initialState });

// alternative way
export const [SpiderManContext, SpiderManProvider] = createSubscriberContext({ initialState });
// App.tsx
import { MovieCounterComponent } from "path/to/MovieCounterComponent";
import { NameComponent } from "path/to/NameComponent";
import { SpiderManProvider } from "path/to/SpiderManContext";

const App = (): ReactElement => {
  return (
    <SpiderManProvider>
      <div>
        <NameComponent />
        <MovieCounterComponent />
      </div>
    </SpiderManProvider>
  );
};

Basic usage

// MovieCounterComponent.tsx
import { useSubscribe } from "react-subscribe-context";
import { SpiderManContext } from "path/to/SpiderManContext";

export const MovieCounterComponent = (): ReactElement => {
  const [movieCounter, setMovieCounter] = useSubscribe(SpiderManContext, "movieCounter");
  // alternative way
  const { value: movieCounter, setValue: setMovieCounter } = useSubscribe(
    SpiderManContext,
    "movieCounter"
  );

  const handleClickCounter = () => {
    setMovieCounter(movieCounter + 1);
  };

  return <button onClick={handleClickCounter}>{movieCounter}</button>;
};

Subscribing to nested object values

These components will subscribe to first and last value changes. Even if the name object itself changes, the components will not rerender unless the first or last values are different. The examples below show two different ways of subscribing to a nested value.

// FirstNameComponent.tsx
import { useSubscribe } from "react-subscribe-context";
import { SpiderManContext } from "path/to/SpiderManContext";

export const FirstNameComponent = (): ReactElement => {
  const [user] = useSubscribe(SpiderManContext, "user");
  // alternative way
  const { value: user } = useSubscribe(SpiderManContext, "user");

  const {
    name: { first },
  } = user;

  return <div>{first}</div>;
};
// LastNameComponent.tsx
import { useSubscribe } from "react-subscribe-context";
import { SpiderManContext } from "path/to/SpiderManContext";

export const LastNameComponent = (): ReactElement => {
  const [state] = useSubscribe(SpiderManContext);
  // alternative way
  const { state } = useSubscribe(SpiderManContext);

  const {
    user: {
      name: { last },
    },
  } = state;

  return <div>{last}</div>;
};
// NameComponent.tsx
import { ReactElement } from "react";
import { useSubscribe } from "react-subscribe-context";
import { SpiderManContext } from "path/to/SpiderManContext";
import { FirstNameComponent } from "path/to/FirstNameComponent";
import { LastNameComponent } from "path/to/LastNameComponent";

type Name = { first: string; last: string };

const spiderManNames: Name[] = [
  { first: "Peter", last: "Parker" },
  { first: "Peter", last: "Porker" },
  { first: "Peni", last: "Parker" },
  { first: "Miles", last: "Morales" },
];

const getRandomSpiderManName = (currentName: Name) => {
  let randomName: Name = spiderManNames[0];

  do {
    randomName = spiderManNames[Math.floor(Math.random() * spiderManNames.length)];
  } while (currentName.first === randomName.first && currentName.last === randomName.last);

  return randomName;
};

export const NameComponent = (): ReactElement => {
  const [, setContextState] = useSubscribe(SpiderManContext);

  const handleClickRandomizeName = () => {
    setContextState((prevState) => {
      let {
        user: { name },
      } = prevState;

      const randomSpiderManName = getRandomSpiderManName(name);

      return {
        ...prevState,
        user: {
          ...prevState.user,
          name: randomSpiderManName,
        },
      };
    });
  };

  return (
    <div>
      <button onClick={handleClickRandomizeName}>Randomize name</button>
      <FirstNameComponent />
      <LastNameComponent />
    </div>
  );
};

Accessing state without subscribing to a value

// NameComponent.tsx
import { useSubscribe } from "react-subscribe-context";
import { SpiderManContext } from "path/to/SpiderManContext";
import { FirstNameComponent } from "path/to/FirstNameComponent";
import { LastNameComponent } from "path/to/LastNameComponent";

export const NameComponent = (): ReactElement => {
  const [, , contextControl] = useSubscribe(SpiderManContext);
  // alternative way
  const { contextControl } = useSubscribe(SpiderManContext);
  // another alternative way
  const contextControl = React.useContext(SpiderManContext);

  const { getState, setState } = contextControl;

  const handleClickRandomizeName = () => {
    setContextState((prevState) => {
      let {
        user: { name },
      } = prevState;

      const randomSpiderManName = getRandomSpiderManName(name);

      return {
        ...prevState,
        user: {
          ...prevState.user,
          name: randomSpiderManName,
        },
      };
    });
  };

  return (
    <div>
      <FirstNameComponent />
      <LastNameComponent />
      <button onClick={handleClickRandomizeName}>Randomize name</button>
    </div>
  );
};

Adding resuable actions

// SpiderManContext.ts
import { createSubscriberContext, BaseContextControl } from "react-subscribe-context";

const initialState = {
  user: {
    name: {
      first: "Peter",
      last: "Parker",
    },
  },
  movieCounter: 9,
};

const createActions = (baseContextControl: BaseContextControl<typeof initialState>) => {
  const { setValue } = baseContextControl;

  return {
    incrementMovieCounter: () => {
      setValue("movieCounter", (movieCounter) => movieCounter + 1);
    },
  };
};

export const [SpiderManContext, SpiderManProvider] = createSubscriberContext({
  initialState,
  createActions,
});

Using actions

// MovieCounterComponent.tsx
import { useSubscribe } from "react-subscribe-context";
import { SpiderManContext } from "path/to/SpiderManContext";

export const MovieCounterComponent = (): ReactElement => {
  const { value: movieCounter, actions } = useSubscribe(SpiderManContext, "movieCounter");

  return <button onClick={actions.incrementMovieCounter}>{movieCounter}</button>;
};

ContextControl Reference

The ContextControl object holds functions that allows you to get and set values of your state. This is a great way to manage your state without subscribing to a value.

Access via useContext

const contextControl = useContext(MyControlContext);

Access via useSubscribe

const [state, setState, contextControl] = useSubscribe(MyControlContext);
const { state, setState, contextControl } = useSubscribe(MyControlContext);
const [value, setValue, contextControl] = useSubscribe(MyControlContext, "key");
const { value, setValue, contextControl } = useSubscribe(MyControlContext, "key");

ContextControl functions

getValue(key)

Returns a value from your state based on the given key.

Parameter Type Description
key string Required. Field key of your state

getState()

Returns the state of your context

setValue(key, nextValue)

Sets a value in your state for the given field key.

Parameter Type Description
key string Required. Field key of your state
nextValue typeof state[key] Required. New value for state[key]
nextValue (currValue: state[key], currState: typeof state) => typeof state[key] Required. Function that returns the next value

setState(nextState)

Sets values of your state

Parameter Type Description
nextState Partial<typeof state> Required. Next state
nextState (currState: typeof state) => Partial<typeof state> Required. Function that returns the next state

actions

Holds the actions created by the developer from the create subscriber context stage.

Demo

Here's a demo.

Be sure to turn on render highlighting with your React Dev Tool to see the differences between rendering performances between each example.