Easy Shared Reactive State in React without External Libraries
Tags: react, webdev, nextjs, state
Published: 2023-10-06 03:04:13

Right now using `useState` with `useContext` requires a LOT of boilerplate. For every context you have to custom provider, which as we have seen, can be a pain in the but. For what ever reason, Facebook refuses to fix this, so we have other libraries:
- [Redux](https://react-redux.js.org/)
- [Jotai](https://jotai.org/)
- [Zustand](https://docs.pmnd.rs/zustand/getting-started/introduction)

These are the ones I found helpful, but ultimately you're still using an external library to do something React already does itself.  Surely there is a way to do this in React without all the boilerplate etc!?

### useProvider

So I created the `useProvider` hook in the [previous post](https://dev.to/jdgamble555/freakin-easy-react-state-management-with-useprovider-hook-5a0n) in the series as an experiment. It basically uses a map to store objects so that you can retrieve them whenever you want. 

That post was probably one of my least popular posts, and I realized why.  You can't store a `useState` object in a map, at least not while keeping the reactivity.

As I always planned, I went back and thought about it to figure out how to do it. 

🤔💡💭

What if we store the contexts in the map instead of the state itself? Ok, honestly, I'm not sure what my brain was thinking, but I some how (maybe accidently) figured out how to do that. You still have one universal provider, and you can grab any state (even reactive) by the context. Review the previous posts to see this evolution:

### use-provider.tsx
```ts
'use client';

import {
    FC,
    ReactNode,
    createContext,
    useContext,
    type Context,
    useState
} from "react";

const _Map = <T,>() => new Map<string, T>();
const Context = createContext(_Map());

export const Provider: FC<{ children: ReactNode }> = ({ children }) =>
    <Context.Provider value={_Map()}>{children}</Context.Provider>;

const useContextProvider = <T,>(key: string) => {
    const context = useContext(Context);
    return {
        set value(v: T) { context.set(key, v); },
        get value() {
            if (!context.has(key)) {
                throw Error(`Context key '${key}' Not Found!`);
            }
            return context.get(key) as T;
        }
    }
};
```
This is the same code from the [first post](https://dev.to/jdgamble555/freakin-easy-react-state-management-with-useprovider-hook-5a0n), just renamed to `useContextProvider`.  However, now we are going to use this as a helper function for the real `useProvider` hook:
```ts
export const useProvider = <T,>(key: string, initialValue?: T) => {
    const provider = useContextProvider<Context<T>>(key);
    if (initialValue !== undefined) {
        const Context = createContext<T>(initialValue);
        provider.value = Context;
    }
    return useContext(provider.value);
};
```
Here is what is happening. The `useContextProvider` just creates a universal provider that can store anything in a map. Again, see the [first post](https://dev.to/jdgamble555/freakin-easy-react-state-management-with-useprovider-hook-5a0n). `useProvider` creates a new context for whatever value is passed in, and sets that as a value to the key you pass in. I know this sounds confusing, so imagine this:

### container
```html
<Provider>
---- my app components
</Provider>
```
### simplified set value (pseudocode)
```ts
// create a new map and set that as value of universal provider
const providers = new Map()
providers.set('count', createContext(0))
<Context.Provider value={provider} />
```
### simplified get value (pseudocode)
```ts
// get the 'count' key from universal provider
// which returns a context, use that context to get counter
const providers = useContext(Provider)
const countContext = providers.get('count')
const counter = useContext(countContext.value)
```
I'm not sure if that makes sense, but that is in its simplest form what is happening. To use it, you simply call it like this:

### Parent
```ts
// create a state context
const state = useState(0);
useProvider('count', state);
```
### Child
```ts
const [count, setCount] = useProvider('count')
```
And that's it!!!

You can have as many providers you want with ONE SINGLE UNIVERSAL PROVIDER. Just name it whatever you like. No more `context hell`!

However, I didn't stop there. You pretty much are always going to want to share state, so why not make that automatic too!

```ts
export const useSharedState = <T,>(key: string, initialValue?: T) => {
    let state = undefined;
    if (initialValue !== undefined) {
        const _useState = useState;
        state = _useState(initialValue);
    }
    return useProvider(key, state);
};
```
This helper function will allow you to just use the provider like a state hook anywhere! 

### Parent
```ts
const [count, setCount] = useSharedState('count', 0);
```
### Child / Sibling / Grand Child
```ts
const [count, setCount] = useSharedState<number>('count');
```
Remember `count` is the context name, and `0` is the initial value. That's literally it! _Works like a charm everywhere_. You still need to include the ONE UNIVERSAL PROVIDER in your root:

### page.tsx
```ts
import Test from "./test";
import { Provider } from "./use-provider";

export default function Home() {

  return (
    <Provider>
      <Test />
    </Provider>
  );
}
```
# Final Code
### use-provider.tsx
```ts
'use client';

import {
    FC,
    ReactNode,
    createContext,
    useContext,
    type Context,
    useState
} from "react";

const _Map = <T,>() => new Map<string, T>();
const Context = createContext(_Map());

export const Provider: FC<{ children: ReactNode }> = ({ children }) =>
    <Context.Provider value={_Map()}>{children}</Context.Provider>;

const useContextProvider = <T,>(key: string) => {
    const context = useContext(Context);
    return {
        set value(v: T) { context.set(key, v); },
        get value() {
            if (!context.has(key)) {
                throw Error(`Context key '${key}' Not Found!`);
            }
            return context.get(key) as T;
        }
    }
};

export const useProvider = <T,>(key: string, initialValue?: T) => {
    const provider = useContextProvider<Context<T>>(key);
    if (initialValue !== undefined) {
        const Context = createContext<T>(initialValue);
        provider.value = Context;
    }
    return useContext(provider.value);
};

export const useSharedState = <T,>(key: string, initialValue?: T) => {
    let state = undefined;
    if (initialValue !== undefined) {
        const _useState = useState;
        state = _useState(initialValue);
    }
    return useProvider(key, state);
};
```
This is not a lot of code for the power it provides! It will save you so much boilerplate!

Note: I did a trick above for conditional `useState` by setting it as an uncalled function first if you find that interesting :)

- [GitHub repo](https://github.com/jdgamble555/react-use-provider)
- [StackBlitz](https://stackblitz.com/github/jdgamble555/react-use-provider?file=app%2Fuse-provider.tsx) - (if loading error, use CodeSandBox)
- [CodeSandBox](https://codesandbox.io/p/github/jdgamble555/react-use-provider/master?file=%2Fapp%2Fuse-provider.tsx)

![Counter useProvider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckz4tpeorqi05m1d7edy.png)

I'm sure I missed something here, but this seems to be amazing. If I ever decide to actually use react (I love Svelte and Qwik!), I would definitely use this custom hook: `useProvider`. 

Let me know if I missed something!

J

Current rebuilding [code.build](https://code.build)

### Update 3-10-24
Here is a compilate for a reusable `use-shared.ts` file.
```ts
'use client';

import {
    FC,
    ReactNode,
    createContext,
    useContext,
    type Context
} from "react";

const _Map = <T,>() => new Map<string, T>();
const Context = createContext(_Map());

export const Provider: FC<{ children: ReactNode }> = ({ children }) =>
    <Context.Provider value={_Map()}>{children}</Context.Provider>;

const useContextProvider = <T,>(key: string) => {
    const context = useContext(Context);
    return {
        set value(v: T) { context.set(key, v); },
        get value() {
            if (!context.has(key)) {
                throw Error(`Context key '${key}' Not Found!`);
            }
            return context.get(key) as T;
        }
    }
};

export const useShared = <T, A>(
    key: string,
    fn: (value?: A) => T,
    initialValue?: A
) => {
    const provider = useContextProvider<Context<T>>(key);
    if (initialValue !== undefined) {
        const state = fn(initialValue);
        const Context = createContext<T>(state);
        provider.value = Context;
    }
    return useContext(provider.value);
};
```