Best way to do Cross Component Communication in React Dashboards

pravchuk
engineering-udaan
Published in
5 min readFeb 13, 2023

Problem : Build a dashboard in React (with multiple components talking to each other)

Usual Solution: Redux or ContextApi

When we have a lot of components within a dashboard, many times we would need “Cross Component” communication. Folks, usually go for redux and some useContext sort of implementation which is just state management at a global level.

When we have this central store and modify it, all relevant components which have subscribed to it end up calling render again. (Multiple unnecessary renders calls are not good.)

Drawbacks:

  1. When we have a single state object having multiple values, and any ‘one’ of those properties is changed, it ends up rendering items even those that haven’t changed. This is because the components all subscribe to the parent state object in Redux.
  2. Lots of memoization is necessary to stop these re-renders on all the different components in the dashboard.
  3. It takes higher memory utilisation when doing memoization on every single component used in the dashboard.

Thought process:

Now, we need a solution which will help us easily communicate between components without calling so many renders.

For example:

Let’s say by clicking on a person (on the left) (component A) you want the appropriate numbers to show up (on the right top as marked by the arrow) (component B)

We need a way for component A to tell component B.

In other words,

We need component B to listen to component A.

We may have more than 1 component in which we want to be listening to component A’s interactions.

A PubSub model is more suitable for this type of situation to handle interactions. While on the other hand, we need to supply the common data to all components (incl comp A and B and so on…)

We need some way to segregate interactions from common state management.

Separation for Concerns :

Solution:

We create 2 hooks,

  1. A PubSub model for handling interactions having 2 methods primarily
const {on, emit} = useInteractionsPubSub()

2. A state management hook to just hold the large chuck of data that doesn’t change too often

const {bigDumpOfData} = useCommonData()

InteractionsPubSub Model

When a component wants to subscribe to an event,

const {on} = useInteractionsPubSub()
on('SOMETHING_CLICKED', ({value}) => {setState(value))} , "SUBSCRIPTION_KEY1")

“SUBSCRIPTION_KEY1” is one unique key that we can use for unsubscribing later on

When a component wants to emit/call an event,

const {emit} = useInteractionsPubSub()
emit({value: true})

When a component is going out of view you can unsubscribe,

const {unsubscribe} = useInteractionsPubSub()
unsubscribe("SOMETHING_CLICKED", "SUBSCRIPTION_KEY1") // same key we used when subscribing

CommonState Model

Use this to store the big dump of data that you receive that usually doesn’t change a lot.

const {bigDumpOfData} = useCommonData()
return <Text>{bigDumpOfData.coolText}</Text>

Benefits:

  1. Easy and Effective for the team.
  2. No more unnecessary renders.
  3. Not all event calls would need to call render. Some can just execute to store some value that it could use to refresh later on.
  4. No need to edit a common file for every single implementation of interaction logic.

Full Code: (written in typescript)

InteractionPubSub.tsx

import React, { useContext, useRef, useState } from "react";
// import {
// GlobalHotKey,
// LocalHotKey,
// } from "../helpers/keyboardHotKeys"; // you can add hotkeys if you like

type InterComponentSignal =
| "OPEN_CHAT"
| "OPEN_SCREEN"
| "OPEN_SCREEN_HOTKEY"
| "OPEN_DRAWER";

type EventType =
| InterComponentSignal
// | GlobalHotKey
// | LocalHotKey; // Only add to this if you want

//- - - - - - - - DON'T Touch below this - - - - - - - - //

interface Events {
list: Map<EventType, Map<string, FuncType>>;
on: (
eventType: EventType,
eventAction: FuncType,
eventId: string
) => void;
emit: (eventType: EventType, ...args: any[]) => void;
unsubscribe: (eventType: EventType, eventId: string) => void;
}

const defaultEvent: Events = {
list: new Map<EventType, Map<string, FuncType>>(),
on: (
eventType: EventType,
eventAction: FuncType,
eventId: string
) => {},
emit: (eventType: EventType, ...args: any[]) => {},
unsubscribe: (eventType: EventType, eventId: string) => {},
};

const InteractionsContext = React.createContext<Events>(defaultEvent);

type FuncType = (...args: any[]) => void;

export const InteractionsProvider = ({
children,
}: {
children: JSX.Element;
}) => {
const list = useRef(new Map<EventType, Map<string, FuncType>>());

const on = (
eventType: EventType,
eventAction: FuncType,
eventId: string
) => {
list.current.has(eventType) || list.current.set(eventType, new Map());
if (list.current.get(eventType)) {
list.current.get(eventType)?.set(eventId, eventAction);
}
};

const emit = (eventType: EventType, ...args: any[]) => {
list.current.get(eventType) &&
list.current.get(eventType)?.forEach((cb: FuncType) => {
cb(...args);
});
};

const unsubscribe = (eventType: EventType, eventId: string) => {
list.current.get(eventType) && list.current.get(eventType)?.delete(eventId);
};

return (
<InteractionsContext.Provider
value={{ list: list.current, on, emit, unsubscribe }}
>
{children}
</InteractionsContext.Provider>
);
};

export const useInteractionsPubSub = () => {
// Custom hooks for functional components
const context = useContext(InteractionsContext);
return context;
};

export function withInteractionsContext<PropsType>(Component: any) {
// HOC for class components
return (props: PropsType) => {
const interactionsCtx = useInteractionsPubSub();
return <Component {...props} interactionsCtx={interactionsCtx} />;
};
}

CommonDataState Manager (a.k.a. Dump State Manager)


import React, { useContext, useReducer } from "react";

const defaultDump: DumpReducerStateAndActions = {
dump: undefined,
loading: false,
dispatch: (val: any) => {},
};

interface Dump {} // To change state values - Step 1

interface DumpReducerAction extends Partial<DumpReducerState> {
type: "INIT" | "LOADING"; //To add new action - Step 1 : add action string here
}

const DumpReducer = (
state: DumpReducerState,
action: DumpReducerAction
): DumpReducerState => {
// To change state values - Step 2 : check if everything here is ok
switch (action.type) {
case "LOADING":
return { ...state, loading: true };
case "INIT":
return { ...state, dump: action.dump, loading: false };
default: //To add new action - Step 2 : add a case here
throw new Error(`Unknown action type: ${action.type}`);
}
};

/// - - - - - - DON'T Touch below this for the most situations - - - - - //

interface DumpReducerState {
dump?: Dump;
loading: boolean;
}

const DumpContext =
React.createContext<DumpReducerStateAndActions>(defaultDump);
interface DumpReducerStateAndActions extends DumpReducerState {
dispatch: React.Dispatch<DumpReducerAction>;
}

export const DumpProvider = ({ children }: { children: JSX.Element }) => {
const [state, dispatch] = useReducer(DumpReducer, defaultDump);

const value: DumpReducerStateAndActions = {
dump: state.dump,
loading: state.loading,
dispatch,
};

return <DumpContext.Provider value={value}>{children}</DumpContext.Provider>;
};

export const useDump = () => {
// Custom hooks for functional components
const context = useContext(DumpContext);
return context;
};

export function withDumpContext<PropsType>(Component: any) {
// HOC for class components
return (props: PropsType) => {
const dumpCtx = useDump();

return <Component {...props} dumpCtx={dumpCtx} />;
};
}

--

--

pravchuk
engineering-udaan
0 Followers
Writer for

React. React Native, Vue, Javascript