reobject
v0.0.2
Published
The ReObject library provides a powerful and flexible way to manage state in React applications.
Downloads
1
Readme
ReObject
The ReObject library provides a powerful and flexible way to manage state in React applications.
It enhances state management in React applications by introducing reactivity and observable patterns. Its integration with React's ecosystem and custom hooks makes it a powerful tool for building dynamic and responsive user interfaces.
- Key Advantages
- Why Not Signals
- State Management (create and use)
- In combination with React hooks
- Reacting to state changes outside React (watch)
- Using computed values
Key Advantages
- Avoid complexity that
useState
,React Context
and state management libraries introduce - Directly mutate state, just like in vanilla JavaScript
- You don't have to refactor all of your codebase. Reobject works with all your
useEffect
,useCallback
anduseMemo
hooks - Full TypeScript support
- Learning curve (if you know React, you know ReObject)
Why not Signals?
One of the key advantages of ReObject
is its ability to handle mutations of nested objects at any depth, providing a level of flexibility and control that is typically not as straightforward with Signals.
Simply put, using ReObject
is as simple as mutating any JavaScript object.
Also ReObject
works with standard
Check this section to see more detailed breakdown between ReObject
and @preact/signals-react
Using ReoBject for state management (create and use)
In this concise example, we create a Todos component to illustrate the seamless integration of both global and local state using ReObject. This showcases the ease of managing reactive states in a React application.
import { use, create } from 'reobject';
// This is the global state
const todoState = create({
todos: [
{ name: 'Stop using signals', checked: false },
{ name: 'Start using ReObject', checked: true },
],
});
export const TodosComponent = () => {
// Using global state defined above
// Other components will share this state
const todos = use(todoState.todos);
// Using local state
const newTodo = use({ name: '' });
return (
<div>
<div>
<input
type="text"
value={newTodo.name}
// We can mutate the state directly
onChange={({ target }) => (newTodo.name = target.value)}
/>
{/* We can directly push the new todo to the list */}
<button
onClick={() => {
todos.push({ name: newTodo.name, checked: false });
newTodo.name = '';
}}
>
Add todo
</button>
</div>
<ul>
{todos.map((todo) => (
<li
key={todo.name}
style={{ color: todo.checked ? 'blue' : 'red' }}
// Isn't this amazing, we can directly mutate todo
onClick={() => (todo.checked = !todo.checked)}
>
{todo.name}
</li>
))}
</ul>
</div>
);
};
As you can see, to create a global state, we use the create function. We can then pass its output to the use function, and it's as simple as that.
// This is global state
const todoState = create({
todos: [
{ name: 'Stop using signals', checked: false },
{ name: 'Start using ReObject', checked: true },
],
});
export const TodosComponent = () => {
// Using global state
const fullState = use(todoState);
const todos = use(todoState.todos);
// YES, even this is possible
const firstTodo = use(todoState.todos[0]);
// .....
};
If we pass an object literal (not value returned by the create
function) directly to the use function instead of a global state, it will initialize a local state to be used within that specific component.
export const TodosComponent = () => {
// This is local state
const newTodo = use({ name: '' });
// .....
};
In combination with React hooks
To integrate ReObject's reactive state with React hooks like useEffect, useMemo, and useCallback, simply import these hooks from ReObject instead of React.
import { useEffect, useCallback, useMemo } from 'reobject';
ReObject's versions of these hooks are designed to detect changes in the reactive state efficiently. This means you only need to change the source of your hook imports, with no other modifications required in your codebase.
This seamless integration ensures that your existing code structure can easily adapt to ReObject's reactive state management.
Reacting to state changes outside React (watch)
In ReObject, the watch() function provides a robust way to observe changes in state outside of React components.
A practical example of this is persisting state changes to local storage.
export const globalState = create({
userSettings: {
theme: 'light',
notificationsEnabled: true,
},
});
// Whenever the value of userSettings change, it will be persisted in localStorage
watch(globalState.userSettings, (newSettings) => {
localStorage.setItem('settings', JSON.stringify(newSettings));
});
Computed values
In the provided code example, we utilize ReObject's watch function to create a computed value based on the state. The computed variable returned from watch contains two properties: .value and .stop.
When the callback provided to the watch function does not return a value, it is used similarly to useEffect. This approach is suitable for executing side effects in response to state changes.
On the other hand, when the callback does return a value, the usage of watch is akin to useMemo, albeit outside the scope of React components. This allows for the creation of computed values that automatically update when their dependencies change.
computed.value
: This property holds the current computed value. In this example, it's the result of multiplying the state.counter by 10. Whenever state.counter changes, the computation inside the watch callback is executed, andcomputed.value
is updated accordingly. So, as state.counter increments,computed.value
reflects the new computed value (10 times the current counter).computed.stop()
: This method is used to stop watching for changes in the state. Whencomputed.stop()
is called, it disconnects the computed value from the state. This means that subsequent changes to state.counter will no longer affectcomputed.value
. In the example, after callingcomputed.stop()
, further increments to state.counter do not altercomputed.value
, which remains at the last computed value before stop was called.
In summary, .value provides the current computed value based on the state, and .stop is used to cease the computation and disassociate it from further state changes.
export const state = create({
counter: 1,
});
// Whenever state changes, computed.value will change
const computed = watch(state, (newState) => {
return newState.counter * 10;
});
// state.counter === 1
// computed.value === 10
state.counter++;
// state.counter === 2
// computed.value === 20
state.counter++;
// state.counter === 3
// computed.value === 30
// We can stop watching for the changes
computed.stop();
state.counter++;
// state.counter === 4
// computed.value === 30
state.counter++;
// state.counter === 5
// computed.value === 30
Compared to Signals
To illustrate the difference between ReObject and the Signals library like @preact/signals-react
, let's consider an example where we're dealing with nested object mutations.
Example with ReObject:
In ReObject, you can directly mutate nested objects, and the library will handle the updates and reactivity seamlessly.
import { create, use } from 'reobject';
// Initial state with nested objects
const initialState = {
user: {
name: 'Alice',
address: {
city: 'Wonderland',
zip: '12345',
},
},
};
// Creating an observable state
const state = create(initialState);
// Component using the state
function UserProfile() {
const user = use(state.user);
const updateCity = () => {
// Direct mutation of a nested object
user.address.city = 'New Wonderland';
};
return (
<div>
<p>User City: {user.address.city}</p>
<button onClick={updateCity}>Update City</button>
</div>
);
}
Attempt with Signals Library:
The Signals library, such as @preact/signals-react, doesn't support direct mutation of nested objects in the same way. You'd typically have to replace the entire nested object or use a more complex approach.
import { createSignal } from '@preact/signals-react';
// Initial state with nested objects
const user = createSignal({
name: 'Alice',
address: {
city: 'Wonderland',
zip: '12345',
},
});
// Component using the state
function UserProfile() {
// Signals library requires a different approach for nested updates
const updateCity = () => {
// Attempting direct mutation like this won't work as expected
user.value.address.city = 'New Wonderland';
};
return (
<div>
<p>User City: {user.value.address.city}</p>
<button onClick={updateCity}>Update City</button>
</div>
);
}