Simple and powerful state management library for React projects.
- Simple and straightforward structure.
- Async state updates.
- Multiple state updates in sequence (including async updates).
- No redundant re-renders.
- No providers.
npm i ovalo-react
- Segments holds the app global state data.
- Segments data structure:
const segments = {
counter: {
state: 0,
},
};
- Initializing the segments data:
useInitSegments( segments );
- Consume a segment state data:
const { state, dispatch } = useSegment( 'counter' );
- Dispatch a state change:
dispatch( ( prevState ) => prevState + 1 );
- Dispatch an async state change:
dispatch( ( prevState ) => new Promise( ( res ) => {
setTimeout( () => res( prevState + 5 ), 2000 );
} ) );
- Dispatch a sequence (gradually executed):
const action1 = ( prevState ) => new Promise( ( resolve ) => {
setTimeout( () => resolve( prevState - 10 ), 2000 );
} );
const action2 = ( prevState ) => new Promise( ( resolve ) => {
setTimeout( () => resolve( prevState - 20 ), 3000 );
} );
dispatch( [ action1, action2 ] );
- Actions that can be executed by the dispatch function:
const segments = {
counter: {
state: 0,
actions: {
add: ( prevState ) => ++prevState,
reduce: ( prevState ) => --prevState,
},
},
};
- Consume actions in the component:
const { state, dispatch, actions } = useSegment( 'counter' );
- Dispatch actions:
dispatch( actions.add );
- Define actions with dynamic values:
const segments = {
counter: {
state: 0,
actions: {
add: ( number ) => ( prevState ) => prevState + number,
reduce: ( number ) => ( prevState ) => prevState - number,
},
},
};
- Dispatch actions with dynamic values:
dispatch( actions.add( 3 ) );
- Dispatch without re-rendering the existing component by using
useDispatch
hook instead of theuseSegment
hook:
const { dispatch, actions } = useDispatch( 'counter' );
- The state structure can be either a primitive, array or an object.
- Initializing segments groups:
useInitSegments( mainSegments, 'main' );
useInitSegments( footerSegments, 'footer' );
- Consuming a segment from group:
const { state, dispatch, actions } = useSegment( 'counter', 'main' );
- The segments state can be exposed in the 'window' level.
- Initializing the semgnets data can be done even before the app is loaded.
The basic concept of the library is to define 'segments' data, which is basically an object that holds the app state.
Creating segments data:
The segments data is the initial state of the app.
Example of App.jsx
file:
import React from 'react';
import { useInitSegments } from 'ovalo-react';
import Counter from './Counter';
const segments = {
counter: {
state: 0,
},
};
export default function App() {
useInitSegments( segments );
return (
<div className="App">
<Counter />
</div>
)
}
The dispatch function can be deconstructured from the useSegment
hook, and can manipulate the state value.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch } = useSegment( 'counter' );
return (
<div>
<button
onClick={ () => dispatch( ( prevState ) => --prevState ) }
>-</button>
<span> { state } </span>
<button
onClick={ () => dispatch( ( prevState ) => ++prevState ) }
>+</button>
</div>
);
}
Each <button>
click dispatches a state update, which makes the state value to be increased/decreased by 1.
The dispatch function supports async state updates by passing a function that returns a Promise. Once the Promise is resolved, the state will be updated.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch } = useSegment( 'counter' );
return (
<div>
<button
onClick={ () => dispatch( ( prevState ) => {
return new Promise( ( resolve ) => {
setTimeout( () => resolve( --prevState ), 2000 );
} );
} ) }
>-</button>
<span> { state } </span>
<button
onClick={ () => dispatch( ( prevState ) => {
return new Promise( ( resolve ) => {
setTimeout( () => resolve( ++prevState ), 2000 );
} );
} ) }
>+</button>
</div>
);
}
Each <button>
click dispatches an async state update, which makes the state value to be increased/decreased by 1 after 2 seconds.
The dispatch function supports an array of multiple state updates that run in a sequence.
It can be useful when a certain state update depends on a prior one to be fulfilled.
For example: waiting for an API request and updating a different state only once the API request is fulfilled.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch } = useSegment( 'counter' );
const action1 = ( prevState ) => new Promise( ( resolve ) => {
setTimeout( () => resolve( prevState - 10 ), 2000 );
} );
const action2 = ( prevState ) => new Promise( ( resolve ) => {
setTimeout( () => resolve( prevState - 20 ), 3000 );
} );
const sequence = [ action1, action2 ];
return (
<div>
<button
onClick={ () => dispatch( sequence ) }
>-</button>
<span> { state } </span>
<button
onClick={ () => dispatch( ( prevState ) => {
return new Promise( ( resolve ) => {
setTimeout( () => resolve( ++prevState ), 2000 );
} );
} ) }
>+</button>
</div>
);
}
When clicking the -
button, the state will be decreased by 10 within 2 seconds, and afterward will be reduced by 20 after 3 seconds (from the moment that the previous action was fulfilled).
In addition to the state, the 'counter' segment can also hold pre-defined actions that can manipulate the state data (the actions has the same capabilities as to whatever is passed to the dispatch function).
Example of App.jsx
file:
import React from 'react';
import { useInitSegments } from 'ovalo-react';
import Counter from './Counter';
const segments = {
counter: {
state: 0,
actions: {
add: ( prevState ) => ++prevState,
reduce: ( prevState ) => --prevState,
},
},
};
export default function App() {
useInitSegments( segments );
return (
<div className="App">
<Counter />
</div>
)
}
In order to dispatch the actions, they should be deconstructured from the useSegment
hook, and should be passed as the dispatch function argument.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch, actions } = useSegment( 'counter' );
const { add, reduce } = actions;
return (
<div>
<button
onClick={ () => dispatch( add ) }
>-</button>
<span> {state} </span>
<button
onClick={ () => dispatch( reduce ) }
>+</button>
</div>
);
}
By dispatching the 'counter' segment actions, the state value will be increased/decreased by 1.
In some cases you might need the ability to control the actions values from outside.
In this case, each action should return a function that returns an inner function.
The outer function will hold the dynamic value while the inner function will hold the prevState.
Example of App.jsx
file:
import React from 'react';
import { useInitSegments } from 'ovalo-react';
import Counter from './Counter';
const segments = {
counter: {
state: 0,
actions: {
add: ( number ) => ( prevState ) => prevState + number,
reduce: ( number ) => ( prevState ) => prevState - number,
},
},
};
export default function App() {
useInitSegments( segments );
return (
<div className="App">
<Counter />
</div>
)
}
In this case, each action can affect the state with a different value.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch, actions } = useSegment( 'counter' );
const { add, reduce } = actions;
return (
<div>
<button
onClick={ () => dispatch( reduce( 2 ) ) }
>-</button>
<span> {state} </span>
<button
onClick={ () => dispatch( add( 3 ) ) }
>+</button>
</div>
);
}
Each click on the +
button will increase the state value by 3, while each click on the -
button will decrease the value by 2.
In some cases, actions might need to perform an async state updates. The actions functions can return a Promise, that will update the state value once it's resolved.
Example of App.jsx
file:
import React from 'react';
import { useInitSegments } from 'ovalo-react';
import Counter from './Counter';
const segments = {
counter: {
state: 0,
actions: {
add: ( number ) => ( prevState ) => {
return new Promise( ( resolve ) => {
setTimeout( () => resolve( prevState + number ), 2000 );
} );
},
reduce: ( number ) => ( prevState ) => {
return new Promise( ( resolve ) => {
setTimeout( () => resolve( prevState - number ), 1000 );
} );
},
},
},
};
export default function App() {
useInitSegments( segments );
return (
<div className="App">
<Counter />
</div>
)
}
By disptaching the add
action, the state will be changes after 2 seconds, while when dispatching the reduce
action the state will be changed after 1 second.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch, actions } = useSegment( 'counter' );
const { add, reduce } = actions;
return (
<div>
<button
onClick={ () => dispatch( reduce( 2 ) ) }
>-</button>
<span> {state} </span>
<button
onClick={ () => dispatch( add( 3 ) ) }
>+</button>
</div>
);
}
There is no change in terms of the async actions dispatch, the state will be increased after 2 seconds and will be decreased after 1 second.
In some cases, a certain component should just update the state without consuming it.
When implementing the useSegment
hook, each dispatch will re-render the existing component because it's bound to the segment state.
Therefore, in order to prevent the existing component re-renders when a state update should be dispatch without consuming the state, use the useDispatch
hook instead of the useSegment
hook.
The useDispatch
hook, holds the dispatch function and the actions object, but does not hold the state, and therefore will not trigger a re-render on each dispatch.
Example of an external Footer.jsx
component file:
import React from 'react';
import { useDispatch } from 'ovalo-react';
export default function Footer() {
const { dispatch, actions } = useDispatch( 'counter' );
const { add, reduce } = actions;
return (
<div>
<h3>Footer Component That Will Not Be Re-rendered:</h3>
<button onClick={ () => dispatch( add( 5 ) ) }>ADD FROM FOOTER</button>
<button onClick={ () => dispatch( reduce( 5 ) ) }>REDUCE FROM FOOTER</button>
</div>
);
}
The footer component can affect the 'counter' state without being re-render on each dispatch, due to not consuming the state and using the useDispatch
hook, instead of the useSegment
hook.
The state structure can be either a primitive, array or an object.
For example:
const segments = {
counter: {
state: 0,
actions: {
add: ( prevState ) => ++prevState,
reduce: ( prevState ) => --prevState,
},
},
todos: {
state: [],
actions: {
addTodo: ( todo ) => ( prevState ) => [ ...prevState, todo ],
},
},
menu: {
state: {
isOpened: false,
items: [],
},
actions: {
toggle: ( prevState ) => ( { ...prevState, isOpened: ! prevState.isOpened } ),
addItem: ( newItem ) => ( prevState ) => ( { ...prevState, items: [ ...prevState.items, newItem ] } ),
},
},
};
The segments data can be configured as multiple groups that manage their state separately.
This can be useful when working with multiple apps that needs to share the same global state, but still manage their own state in a separated scope.
The useInitSegments
hook can get a second argument that defines each group name.
Example of App.jsx
file:
import React from 'react';
import { useInitSegments } from 'ovalo-react';
import Counter from './Counter';
import Footer from './Footer';
const mainSegments = {
counter: {
state: 0,
},
};
const footerSegments = {
counter: {
state: 0,
},
};
export default function App() {
useInitSegments( mainSegments, 'main' );
useInitSegments( footerSegments, 'footer' );
return (
<div className="App">
<Counter />
<Footer />
</div>
)
}
By defining multiple segments groups ('main' and 'footer') each segment state will be managed separately. Meaning, each state update of the 'main' group counter, will not affect the 'footer' group counter.
Notice: additional argument 'main'
was added to the useSegment
hook: useSegment( 'counter', 'main' )
.
Example of a Counter.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Counter() {
const { state, dispatch } = useSegment( 'counter', 'main' );
return (
<div>
<h3>Counter Component:</h3>
<button
onClick={ () => dispatch( ( prevState ) => --prevState ) }
>-</button>
<span> { state } </span>
<button
onClick={ () => dispatch( ( prevState ) => ++prevState ) }
>+</button>
</div>
);
}
The state will be updated only in the Counter
component, without affect the Footer
component (see below).
Notice: additional argument 'footer'
was added to the useSegment
hook: useSegment( 'counter', 'footer' )
.
Example of a Footer.jsx
component file:
import React from 'react';
import { useSegment } from 'ovalo-react';
export default function Footer() {
const { state, dispatch } = useSegment( 'counter', 'footer' );
return (
<div>
<h3>Footer Component:</h3>
<button
onClick={ () => dispatch( ( prevState ) => --prevState ) }
>-</button>
<span> { state } </span>
<button
onClick={ () => dispatch( ( prevState ) => ++prevState ) }
>+</button>
</div>
);
}
The state will be updated only in the Footer
component, without affect the Counter
component.
The segments state can be exposed in the 'window' level, so that external an external source can affect the app state.
By deconstructuring segments
from the useInitSegments
hook, will allow to expose the state in the 'window' level.
Example of App.jsx
file:
import React, { useEffect } from 'react';
import { useInitSegments } from 'ovalo-react';
import Counter from './Counter';
const initialSegments = {
counter: {
state: 0,
actions: {
add: ( prevState ) => ++prevState,
reduce: ( prevState ) => --prevState,
},
},
};
export default function App() {
const { segments } = useInitSegments( initialSegments );
useEffect( () => {
window.segments = segments;
}, [] );
return (
<div className="App">
<Counter />
</div>
)
}
Using the segments in the 'window' level is almost the same as using the useSegment
hook with a few minor differences:
Instead of working with the useSegment
hook, in the window ou should be using: segments.use
.
Note: Dispatching actions will be done in the exact same way as in the react environment.
const { dispatch, actions } = window.segments.use( 'counter' );
const { add, reduce } = actions;
// Actions can be dispatched in the exact same way as in the react environment.
dispatch( add );
// After 2 seconds.
setTimeout( () => {
// The state can also be changed by passing a function in the exact same way as in the react environment.
dispatch( ( prevState ) => prevState - 5 );
}, 2000 );
Instead of the state
value, the segment.use
expose only the initial state value.
Due to not being in a react environment (when working in the 'window' level), there is no state is variable that being updated automatically.
Instead, there are 2 options to get the state data:
initialState
- variable that holds the initial state value.getState
- a function that returns the current state value.
const { initialState, getState dispatch, actions } = segments.use( 'counter' );
dispatch( ( prevState ) => ++prevState );
console.log( 'Initial state value that will not be updated on state changes: ', initialState );
console.log( 'Will return the current state value: ', getState() );
In addition, it's possible to reigtsre a callback function that will be triggered on each state change, and will get the current state value as its argument:
Note: The segments
is the global instance that was created in the App.jsx
component above.
const { dispatch, actions, register } = window.segments.use( 'counter' );
const onStateChange = ( currentState ) => {
console.log( 'The current state value is: ', currentState );
};
register( onStateChange ); // Registering the onStateChange function to be triggered on each state changes.
dispatch( add ); // Changing the state will trigger the registered onStateChange function.
For more information on how to work with the ovalo global state, outsite of the react environment, see the following documentation:
The segments data can also be initialized before the app is loaded by importing Segments
from the library.
The segments data can be initialized before the app is injected to the DOM, or even by any other source that is external to the app.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import Segments from 'ovalo-react';
const initialSegments = {
counter: {
state: 0,
actions: {
add: ( prevState ) => ++prevState,
reduce: ( prevState ) => --prevState,
},
},
};
Segments.init( initialSegments );
ReactDOM.render(
<App />,
document.getElementById('root')
)
Note: There is no differnce in terms of using the state inside the components.