Skip to content

Latest commit

 

History

History
319 lines (253 loc) · 9.04 KB

README.md

File metadata and controls

319 lines (253 loc) · 9.04 KB

react-super-context

A tiny wrapper library around the React Context API that removes a lot of boilerplate required to create and consume contexts.

Features

  • Create contexts with no boilerplate
  • No more nested context providers
  • Throws an error when consuming a context that has no provider instead of failing silently
  • Built with TypeScript
  • Small bundle size

Installation

npm i react-super-context

Before and After

Before

// when using TypeScript, you must define an interface for the context's value to get proper type hints
interface CounterContextModel {
  count: number;
  increment: () => void;
  decrement: () => void;
}

// createContext expects a default value that is used if there are no providers for the context
const CounterContext = createContext<CounterContextModel>({
  count: 0,
  increment: () => {},
  decrement: () => {},
});

// we export a provider component that is responsible for the context's states 
export const CounterContextProvider = ({ children }: PropsWithChildren<{}>) => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(Math.max(0, count - 1));

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </CounterContext.Provider>
  );
};

// we also export a hook that can be used to consume the context in our components
export const useCounter = () => useContext(CounterContext);

After

// createSuperContext returns a custom provider and a hook for consumption
const [CounterContext, useCounter] = createSuperContext(() => {
  // the state logic is the same as before
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(Math.max(0, count - 1));

  // we now simply have to return the context's value
  // when using TypeScript, the types are inferred and the useCounter hook will have proper type hints
  return { count, increment, decrement };
});

export { CounterContext, useCounter };

Before

const App = () => {
  return (
    <CounterContextProvider>
      <MySecondContextProvider>
        <MyThirdContextProvider>
            <div>Your app with consumers comes here</div>
        </MyThirdContextProvider>
      </MySecondContextProvider>
    </CounterContextProvider>
  );
};

After

const App = () => (
  <SuperContext contexts={[CounterContext, MySecondContext, MyThirdContext]}>
    <div>Your app with consumers comes here</div>
  </SuperContext>
);

Examples

1. Simple example

1. Use the createSuperContext function to create your context. It takes a factory function that returns the context's value and returns a context object as well as a hook to consume the state.

// CounterContext.ts
const [CounterContext, useCounter] = createSuperContext(() => {
  const [count, setCount] = useState(0);
  return {count, setCount};
});

export { CounterContext, useCounter };

2. To create a provider for the context, add the SuperContext component in your app and pass it the CounterContext created by the createSuperContext call.

// App.tsx
const App = () => (
  <SuperContext contexts={[CounterContext]}>
    <CountDisplay/>
    <CounterButton/>
  </SuperContext>
);

3. Consume the context in your components using the useCounter hook.

// CountDisplay.tsx
const CountDisplay = () => {
  const { count } = useCounter();
  return <div>{count}</div>;
};

// CounterButton.tsx 
const CounterButton = () => {
  const { count, setCount } = useCounter();
  return <button onClick={() => setCount(count + 1)}>+1</button>;
};

2. Use multiple contexts

1. Create a second context that uses useCounter.

// EvenOrOddContext.ts
const [EvenOrOddContext, useEvenOrOdd] = createSuperContext(() => {
  const { count } = useCounter();
  return count % 2 === 0 ? "even" : "odd";
});

export { EvenOrOddContext, useEvenOrOdd };

2. Remember to add it to the contexts lists. The order of the contexts matters.

// App.tsx
const App = () => (
  <SuperContext contexts={[CounterContext, EvenOrOddContext]}>
    <CountDisplay/>
    <CounterButton/>
  </SuperContext>
);

EvenOrOddContext depends on CounterContext so if they were given the other way around (contexts={[EvenOrOddContext, CounterContext]}), then the useCounter call in EvenOrOddContext.ts will throw an error.

3. Consume the new context.

// CountDisplay.tsx
export const CountDisplay = () => {
  const { count } = useCounter();
  const evenOrOdd = useEvenOrOdd();

  return <div>{count} ({evenOrOdd})</div>;
};

3. Use hooks as you normally would

const [Logging] = createSuperContext(() => {
  const { count } = useCounter();
  const evenOrOdd = useEvenOrOdd();

  useEffect(() => {
    console.log(`The current count is ${count} which is ${evenOrOdd}`);
  }, [count, evenOrOdd]);
});

export default Logging;

Remember to always add your context objects to the SuperContext component.

4. Passing props

1. Create a super context with the desired props.

// CounterContext.ts
interface CounterContextProps {
  initial: number;
}

const [CounterContext, useCounter] = createSuperContext(({ initial }: CounterContextProps) => {
  const [count, setCount] = useState(initial);
  return { count, setCount };
});

export { CounterContext, useCounter };

2. CounterContext is a function that you can pass the props to.

// App.tsx
const App = () => (
  <SuperContext contexts={[CounterContext({ initial: 10 })]}>
    <CountDisplay/>
    <CounterButton/>
  </SuperContext>
);

5. TypeScript

In all the examples above, TypeScript is able to infer the types of both the context's value (the value returned by the factory function and by the generated hook) and the contexts' props.

const CountDisplay = () => {
  const { count } = useCounter(); // inferred type: { count: number, increment: () => void, decrement: () => void }
  const evenOrOdd = useEvenOrOdd(); // inferred type: "even" | "odd"

  return <div>{count} ({evenOrOdd})</div>;
};

However, you can also define types explicitly:

1. Type given explicitly in createSuperContext call.

// CounterContext.ts
interface CounterContextModel {
    count: number;
    increment: () => void;
    decrement: () => void;
}

const [CounterContext, useCounter] = createSuperContext<CounterContextModel>(() => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(Math.max(0, count - 1));

  return { count, increment, decrement };
});

2. Type inferred when consuming the context.

const CountDisplay = () => {
  const { count } = useCounter(); // inferred type: CounterContext
  return <div>{count}</div>;
};

6. Using props with Typescript

The simplest approach is to define the prop type in the argument for the factory function.

interface CounterContextProps {
  initial: number;
}

const [CounterContext, useCounter] = createSuperContext(({ initial }: CounterContextProps) => {
  const [count, setCount] = useState(initial);
  return { count, setCount };
});

If you have defined the context's value type explicitly, you must pass the prop type as the second generic argument (at least until TypeScript gets support for partial type argument inference).

const [CounterContext, useCounter] = createSuperContext<CounterContext, CounterContextProps>(({initial}) => {
  const [count, setCount] = useState(initial);
  return { count, setCount };
});

7. Options

The createSuperContext function takes an optional object as the second argument, allowing you to specify a number of options.

const [CounterContext, useCounter] = createSuperContext(
    () => {
        const [count, setCount] = useState(0);
        return { count, setCount };
    },
    {
        displayName: "MyCounterContext",
        testValue: { count: 0, setCount: () => {} },
    }
);

displayName will be the name of the context provider component in error messages. The testValue is the value returned by the useCounter hook in a test environment. The library will by default check if NODE_ENV === "test" to determine if it is in a test environment, but this can be overridden with the testEnvironment option.

If you use many of the same options on all context provided by a SuperContext, you can use the defaultOptions prop to set defaults:

const App = () => (
    <SuperContext
        contexts={[CounterContext, EvenOrOddContext]}
        defaultOptions={{displayName: "MyContext"}}
    >...</SuperContext>
);

In the example above, both the CounterContext and the EvenOrOddContext provider components will be displayed as "MyContext" in error messages.

More examples

See more examples here.