DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on

Easy Context in React Server Components (RSC)

Continuing the need to have easy context from my other posts, I wanted to find a way to share context on the server as well.

You may have noticed that useContext and createContext do not compile on the server (another millionth reason I love SvelteKit!).

So, there needs to be a way to do this easily in RSC, even though the React Team, who seems to be extremely far away from the actual userbase, decided to depreciate this feature before it was actually implemented.

You can find createServerContext in the latest React as an import, but there is no actual Consumer like there is in a regular context, so I couldn't get it to work. Supposedly it could be shared on the client and server, but only for small strings. The only actual mention of this is in a tweet:

let Lang = createServerContext("lang", "en");
...
<Lang.Provider value={}>
...
use(Lang)
Enter fullscreen mode Exit fullscreen mode

Either way, I would have re-written this to work like my other context, so it is a moot point.

I should also add, the second best solution is to use the server-only-context library (by manvalls) which uses cache under the hood.


Dear React Team (and NextJS Team)

There is a reason there are so many external state libraries for React. The way React works out-of-the-box is terrible and uses too much boilerplate. We love RSC (the people on the React train who won't move to Svelte have no choice) and we love that Vercel wants to add Server Actions, but either of you can fix this problem. No more Context Hell please!

React Team... Let's add signals (or something better) as well please and not worry about memoizing state! Why are you making things hard that don't have to be!!!!? Serious question.

Ok, I digress.


The Solution

So I basically copied the cache idea from above, but simplified it for my use case with the Map from my other solutions. The cache is actually a Map itself, so I believe this is a Map of a Map under the hood.

use-server-provider.tsx

import 'server-only';
import { cache } from 'react';

const serverContext = cache(() => new Map());

export const useServerProvider = <T,>(
    key: string,
    defaultValue?: T
) => {
    const global = serverContext();

    if (defaultValue !== undefined) {
        global.set(key, defaultValue);
    }

    return [
        global.get(key),
        (value: T) => global.set(key, value)
    ];
};
Enter fullscreen mode Exit fullscreen mode

Parent

const [count, setCount] = useServerProvider('count', 23);
Enter fullscreen mode Exit fullscreen mode

Child

const [count] = useServerProvider<number>('count');
Enter fullscreen mode Exit fullscreen mode

However, it is worth noting, unlike a reactive component, if you set the component after you define the count variable, it won't update unless you grab the count variable again. One way to fix this is to use a value object key like in a signal:

import 'server-only';
import { cache } from 'react';

const serverContext = cache(() => new Map());

export const useServerProvider = <T,>(
    key: string,
    defaultValue?: T
) => {
    const global = serverContext();

    if (defaultValue !== undefined) {
        global.set(key, defaultValue);
    }

    return {
        get value() {
            return global.get(key)
        },
        set value(v: T) {
            global.set(key, v);
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Then you could do this:

Parent

const count = useServerProvider('count', 23);

count.value = 27;
Enter fullscreen mode Exit fullscreen mode

Child

export default function TestChild() {

    const count = useServerProvider<number>('count');

    return (
        <p>Child: {count.value}</p>
    )
}
Enter fullscreen mode Exit fullscreen mode

Nevertheless, on a server you shouldn't care about any of this... so the original react-like way still stands.

Probably the last article in this series... probably...

J

Getting back to rebuilding code.build

Top comments (0)