<- GOBACK

Communication between components

React Server Components have a limitation when it comes to lifting state up to the most common ancestor. There might be a server boundary blocking this. 3 great ways to work around this have been explained in depth by this article. I wanted to share this article because these techniques might be dismissed as solving a problem for RSC's. But they also immensely clean up your regular react applications.

Wrapping the state into a provider

If you just keep lifting state up you'll eventually get a page component that contains a lot of logic. It will be unclear what parts of the ui this logic belongs to. To reuse the same kind of logic in another page you might copy over the parts you need or wrap it into a react hook. But you'll still have to pass all of the state and state setters down through multiple layers of components that will break every time you want to change the data passing through. This middle layer of components will get difficult to scan because they get bloated with extra props.

Moving this logic to context means you can build an easy to reuse provider, a set of named hooks that let any child component opt into accessing the state, and reusable react components that (under the hood) consume that state.

You are now free to compose your components however you want without having to drill props anywhere.

Example

This is a very common piece of code that shares some state with multiple child components

The way state is passed through is called prop drilling.

Use react context to make the state accessible to components down the tree.

Create a dedicated provider and components that consume your context. This will make your state reusable across your application.

Then add dedicated components to consume your provider

This is a very common piece of code that shares some state with multiple child components

The way state is passed through is called prop drilling.

Use react context to make the state accessible to components down the tree.

Create a dedicated provider and components that consume your context. This will make your state reusable across your application.

Then add dedicated components to consume your provider

index.tsx

function Main() {
const [value, setValue] = useState<Value>();
return (
<Page>
<Sidebar
value={value}
setValue={setValue}
/>
<Renderer value={value} />
</Page>
);
}
type ValueProps = {
value: Value;
setValue: Dispatch<SetStateAction<Value>>;
};
function Sidebar({ value, setValue }: ValueProps) {
return (
<Input
value={value}
onChange={setValue}
/>
);
}

Result

index.tsx
valueContext.tsx

function Main() {
return (
<Page>
<ValueProvider>
<Sidebar />
<ValueRenderer />
</ValueProvider>
</Page>
);
}
function Sidebar() {
return (
<ValueInput />
);
}

The dedicated provider and the named hooks is where you would move any logic related to how this state can update and any side effects that should happen. This brings together all related logic in instead of sprinkling multiple flows through each other.