reobject
TypeScript icon, indicating that this package has built-in type declarations

0.0.2 • Public • Published

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

  • 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 and useMemo 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, and computed.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. When computed.stop() is called, it disconnects the computed value from the state. This means that subsequent changes to state.counter will no longer affect computed.value. In the example, after calling computed.stop(), further increments to state.counter do not alter computed.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>
   );
}

Readme

Keywords

none

Package Sidebar

Install

npm i reobject

Weekly Downloads

2

Version

0.0.2

License

none

Unpacked Size

56.9 kB

Total Files

5

Last publish

Collaborators

  • dejansandic