Ultrafast react store library, that prevent unnecessary renders.
Main features:
- lightweight
- easy to use
- handles unnecessary re-renders
The common problem of many store libraries and React.Context
is unnecessary re-renders of child components upon every state change. With React.Context
, all the children using this context will be rendered every time the provider value changes, even if they rely on unaffected parts of the store. And in high-load apps, performance issues become even more acute: unnecessary re-renders may cause notable FPS drops or even freeze the main thread.
VEDRO tackles this problem and triggers re-renders only when those portions of the store used in a component are updated.
VEDRO has all the necessary tools out-of-the-box. It provides createVedro
function to create a context instance and a number of flexible hooks to access and mutate data.
mainStore.ts
import { createVedro } from "vedro";
interface IMainStore {
count: number;
str: string;
str2: string;
}
const initialState: IMainStore = {
count: 1,
str: "Hello",
str2: "This string does not change",
};
export const {
Context: MainStoreContext,
Provider: MainStoreContextProvider,
useStore: useMainStore,
useSelector: useMainStoreSelector,
useDispatch: useMainStoreDispatch,
} = createVedro(initialState);
That's it. Context and hooks are created. Now we need to wrap our components with MainStoreContextProvider
. For example, in our HomePage
component:
HomePage.tsx
import { MainStoreContextProvider } from "mainStore"; // a file with the context
import { Counter } from "./Counter"; // some counter component
import { InputSection } from "./InputSection"; // some input component
export default async function HomePage() {
return (
<main >
<MainStoreContextProvider state={{ count: 5 }}>
<Counter />
<InputSection />
</MainStoreContextProvider>
</main>
);
useSelector
works just like in any other similar libraries. It takes a selector callback and returns an object with a portion of the state specified in the selector callback:
const { count } = useMainStoreSelector((state) => ({ count: state.count }));
useDispatch
returns a dispatch
function for state mutation, which can be used in multiple ways:
const dispatch = useMainStoreDispatch();
// first example
const onClick = () => {
dispatch("count", 7);
dispatch("str", "new value");
};
// second example
const onClick = () => {
dispatch({ count: 1, str: "new value" });
};
// third example
const onClick = () => {
dispatch((state) => ({
count: state.count + 1, // only a part of state we need to change is returned
str1: "new value for str1",
}));
};
As you might notice in the third example, the callback function passed to dispatch
does not return the entire store – only the portion that changed.
The dispatch callback can also be async:
// in this example we have store with serveral fields:
// status - request status
// userData - user data fetched from server
const dispatch = useStoreDispatch();
const onClick = () => {
dispatch(async (state) => {
dispatch("loading", true);
try {
const userData = await getUserData(); // fetching user data
dispatch("userData", userData);
} catch (error) {
console.log(error);
}
dispatch("loading", false);
});
};
useStore
hook returns the store instance. It can be used to access the store in non-React functions or classes.
Now let's take a look at the hooks and their usage in components.
The Counter
component:
Counter.tsx
import React from "react";
import { useMainStoreDispatch, useMainStoreSelector } from "mainStore";
export const Counter: React.FC = () => {
const dispatch = useMainStoreDispatch();
const { count } = useMainStoreSelector(({ count }) => ({ count }));
return (
<div>{ count }</p>
<button
onClick={() =>
dispatch(({ count }) => ({ count: count + 1 }))
}
>
Increment
</button>
</div>
);
};
The InputSection
component:
InputSection.tsx
import React from "react";
import { useMainStoreDispatch, useMainStoreSelector } from "mainStore";
export const InputSection: React.FC = () => {
const dispatch = useMainStoreDispatch();
const { str1 } = useMainStoreSelector(({ str1 }) => ({ str1 }));
return (
<div>
<p>{name}</p>
<input
type="text"
value={str1}
onChange={({ target }) => dispatch("str1", target.value)}
/>
</div>
);
};
Now when we click the button in the Counter
component, the count
field gets updated in the main store. However, the only component to be re-rendered is the Counter
. InputSection
is not subscribed to the count
field, thus, no re-renders will be triggered.