Easy Context in React Server Components (RSC)
Jonathan Gamble
Posted on October 10, 2023
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)
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)
];
};
Parent
const [count, setCount] = useServerProvider('count', 23);
Child
const [count] = useServerProvider<number>('count');
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);
}
}
};
Then you could do this:
Parent
const count = useServerProvider('count', 23);
count.value = 27;
Child
export default function TestChild() {
const count = useServerProvider<number>('count');
return (
<p>Child: {count.value}</p>
)
}
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
Posted on October 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.