textcon
v0.0.4
Published
A react state management library that uses context and provide selector support for fine grainded reactivity
Downloads
33
Maintainers
Readme
text-con
A react state management library that leverages react context but with fine grained reactivity 🚀
textcon
textcon is a simple state-management library that is built on top of react context but provide addional essential features such as:
- Global state management -
textconprovides a global state management that can be used to store data that is shared across the application. - Local state management -
textconprovides a local state management that can be used to store data that is only used in a specific component or a group of components. For instance a form data or data for a specific route. - Fine grained reactivity -
textconsupport selectors out of the box to precisely control the reactivity of your components. - Observeables - library allows you to observe changes in the state outside of the react component tree and react to them.
Installation
# using npm
npm install textcon react
# using yarn
yarn add textcon react
# using pnpm
pnpm add textcon reactMake sure to install
reactas well sincetextcondepends on it.React>=17is required.
Usage
Simple usage
Create context with some default value
import { createContextStore } from "textcon";
// create a context store with initial value of 0
const { Provider, useStore } = createContextStore(0);Wrap your component tree with the provider. It's up to you where you want to wrap your component tree with the provider. You can wrap the entire app or just a part of it.
const App = () => {
return (
<Provider>
<Counter />
</Provider>
);
};Use the useStore hook to access the state and the set (setter) function to update the state.
import { FC } from "react";
const Counter: FC = () => {
const { get: count, set } = useStore();
return (
<div>
<h1>{count}</h1>
<button onClick={() => set((prev) => prev + 1)}>Increment</button>
</div>
);
};Using selectors
The primary feature that differs textcon from the default react context is the support for selectors. With selectors, you can precisely control the reactivity of your components.
import { createContextStore } from "textcon";
// create a context store with initial value
const { useStore, Provider } = createContextStore({
user: {
firstName: "John",
lastName: "Doe",
age: 20,
},
hobbies: ["reading", "coding", "gaming"],
});
const User = () => {
const { get: user } = useStore((state) => state.user);
return (
<div>
<h1>{user.firstName}</h1>
<h1>{user.lastName}</h1>
<h1>{user.age}</h1>
</div>
);
}
const Hobbies = () => {
const { get: hobbies } = useStore((state) => state.hobbies);
return (
<div>
<ul>
{hobbies.map((hobby) => (
<li key={hobby}>{hobby}</li>
))}
</ul>
</div>
);
}
const UserControls() {
const { set } = useStore();
return (
<>
<button
onClick={() =>
set((prev) => ({
...prev,
user: {
...prev.user,
firstName: "Jane",
},
}))
}
>
Change first name
</button>
<button
onClick={() =>
set((prev) => ({
...prev,
user: {
...prev.user,
lastName: "Doe",
},
}))
}
>
Change last name
</button>
</>
)
}
const App = () => {
return (
<Provider>
<User />
<UserControls/>
<Hobbies />
</Provider>
);
}If reducing nested state is getting out of hand, you can use the immer library to update the state. See the Using Immer section for more details.
Here is how you can update the UserControls component using immer.
import {produce} from "immer";
const UserControls() {
const { set } = useStore();
return (
<>
<button
onClick={() =>
set(produce(state => {
state.user.firstName = "Jane"
}))
}
>
Change first name
</button>
<button
onClick={() =>
set(produce(state => {
state.user.lastName = "Doe"
})
}
>
Change last name
</button>
</>
)
}
Here the User component will only re-render when the user property of the state changes and the Hobbies component will only re-render when the hobbies property of the state changes. Make sure to pass the whole state to the setter function when updating the state.
With actions
textcon support actions out of the box. Actions are just functions that can be used to update the state. Actions are useful when you want to update the state in multiple places. For instance, you can create an action to update the user's first name and use it in multiple places. So define your actions in one place and use them in multiple places. Actoin could be sync or async. Just call the set and get functions when needed.
Actions has access to setter and getter as the first argument.
import { createContextStore, ActionablePayload } from "textcon";
// create a context store with initial value and actions as second argument
const { useStore, Provider, useActions } = createContextStore(
{
user: {
firstName: "John",
lastName: "Doe",
age: 20,
},
count: 0,
loading: false,
},
{
increment: ({ set, get }) => {
set((prev) => ({
...prev,
count: get().count + 1,
}));
},
decrement: ({ set, get }) => {
set((prev) => ({
...prev,
count: get().count - 1,
}));
},
// the second argument is the action of type ActionablePayload
decrementBy: ({ set, get }, action: ActionablePayload<number>) => {
set((prev) => ({
...prev,
count: get().count - action.payload,
}));
},
updateFirstName: ({ set, get }, action: ActionablePayload<string>) => {
set((prev) => ({
...prev,
user: {
...prev.user,
firstName: action.payload,
},
}));
},
updateUser: async ({ set, get }, action: ActionablePayload<User>) => {
set((prev) => ({
...prev,
loading: true,
}));
await updateUserOnServer(action.payload); // some async operation
set((prev) => ({
...prev,
user: action.payload,
loading: false,
}));
},
// ...
}
);See the Using Immer section for more details.
const { Provider, useStore, useActions } = createContextStore(
{
user: {
firstName: "John",
lastName: "Doe",
age: 20,
email: "",
},
count: 0,
},
{
increment: ({ set }) => {
set(
produce((state) => {
state.count += 1;
})
);
},
decrement: ({ set }) => {
set(
produce((state) => {
state.count -= 1;
})
);
},
incrementBy: ({ set }, action: ActionablePayload<{ by: number }>) => {
console.log(action?.payload || "No payload");
set(
produce((state) => {
state.count = state.count + (action?.payload?.by || 10);
})
);
},
updateFirstName: ({ set }, action: ActionablePayload<string>) => {
set(
produce((state) => {
state.user.firstName = action?.payload || "";
})
);
},
updateLastName: ({ set }, action: ActionablePayload<string>) => {
set(
produce((state) => {
state.user.lastName = action?.payload || "";
})
);
},
asyncAction: async ({ set }, action: ActionablePayload<string>) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
set(
produce((state) => {
state.user.firstName = action?.payload || "";
})
);
},
}
);Global state
State can be preserved between monting and unmounting of Provider. This is useful when you want to preserve the state between routes and don't want the provider at the top of the component tree.
import { createContextStore } from "textcon";
// create a context store with initial value
const { useStore, Provider } = createContextStore(
{
user: {
firstName: "John",
lastName: "Doe",
age: 20,
},
hobbies: ["reading", "coding", "gaming"],
},
{
// ...
},
{
// preserve state between mounting and unmounting of provider
global: true,
}
);Using Immer
Let's just agree that for depply nested objects, passing/coppying the whole state to the setter function is a tedious task. That's why textcon provides a way to use immer to update the state.
import { createContextStore } from "textcon";
// create a context store with initial value
const { useStore, Provider } = createContextStore(
{
user: {
firstName: "John",
lastName: "Doe",
age: 20,
},
hobbies: ["reading", "coding", "gaming"],
},
{
// ...
updateFirstName: ({ set }, action: ActionablePayload<string>) => {
set(
produce((state) => {
state.user.firstName = action.payload;
})
);
},
updateLastName: ({ set }, action: ActionablePayload<string>) => {
set(
produce((state) => {
state.user.lastName = action.payload;
})
);
},
// ...
}
);