store-me
v1.5.0
Published
Fast, flexible and lightweight global state manager for React. Provides two APIs for both basic and more complex cases allowing you to control component updates.
Downloads
156
Maintainers
Readme
Why "Store me"
"Store me" was created to provide easy way of using a global state in React but also giving the option to fine tune if you need to heavily optimize your React app performance. "Store me" is using React Hooks to re-render components with latest state. This package is not working with class based React components.
| Base methods | Helpful methods | Fine tuning for complex cases | | ------------------------- | ------------------------------- | --------------------------------------- | | useStoreMe | getStoreMe | renderStoreMe | | setStoreMe | resetStoreMe | storeMeSubscriber | | | deleteStoreMe | |
Installation
npm i store-me
Wrap your application and provide the initial state
// index.js
import { StoreMe } from "store-me";
import App from "./App";
const initialState = {
isMenuExpanded: false,
language: "en_US",
user: {
name: "John Doe",
age: 21,
is_premium: true,
profit: "$1050",
settings: {
notifications_allowed: 1,
},
},
};
<StoreMe initialState={initialState}>
<App />
</StoreMe>;
Or start with empty state and fill it later.
// index.js
import { StoreMe } from "store-me";
import App from "./App";
<StoreMe>
<App />
</StoreMe>;
Read more about the "syncStateKeys" prop (similar to Redux sync-state functionality). Read more about the "debug" prop and how it can help you.
<StoreMe syncStateKeys={["language", "user"]} debug={[1, 3]}>
useStoreMe
import { useStoreMe, setStoreMe } from "store-me";
const App = () => {
const { user, isMenuExpanded } = useStoreMe("user", "isMenuExpanded");
return (
<div onClick={() => setStoreMe({ isMenuExpanded: !isMenuExpanded })}>
Hello, {user.name}! Click here to toggle the main menu.
</div>
);
};
Use with "Sub accessor"
In everyday React we import the entire "user" object in our component even if it needs only one or a few properties from it. This way we expose our components to many unwanted re-renders and depending on your application it could drastically affect performance.
"Store me" provides a "sub accessor" option. In this case the component will never re-render unless the user name changes.
const App = () => {
const { user } = useStoreMe("user.name");
console.log(user);
/*
user: {
name: "John Doe"
}
*/
return user.name;
};
Use with multiple "Sub accessors"
Accessing more than one nested value is also possible. This way you can "subscribe" only to non frequently updating values.
const App = () => {
const { user } = useStoreMe("user.[name|age|is_premium]");
const { name, age, is_premium } = user;
console.log(user);
/*
user: {
name: "John Doe",
age: 21,
is_premium: true,
}
*/
return (
<div className={is_premium ? "gold-color" : "silver-color"}>
Hey {name}! You are {age} years old, so cool!
</div>
);
};
What if the user is not yet logged in? "Store me" will always provide the expected object structure.
// Current global state:
const globalState = {
isMenuExpanded: false,
language: "en_US",
user: false,
};
const App = () => {
const { user } = useStoreMe("user.settings.notifications_allowed");
console.log(user);
/*
user: {
settings: {
notifications_allowed: undefined
}
}
*/
return <div>Click here to log in</div>;
};
With dynamic accessor
In the following example we will dynamically change the accessor and "StoreMe" will automatically re-subscribe to it.
const globalState = {
0: {
name: "Batman",
year: 1989,
rating: 7.5,
},
1: {
name: "The Matrix",
year: 1999,
rating: 8.7,
},
};
const App = () => {
const [currentMovieId, setCurrentMovieId] = useState(0);
return (
<div>
<div onClick={() => setCurrentMovieId(prevId => prevId + 1)}>
Click here for next movie
</div>
<MovieView movie_id={currentMovieId} />
</div>
);
};
const MovieView = movie_id => {
const { [movie_id]: movie } = useStoreMe(movie_id);
return (
<div>
Movie {movie.name} - {movie.year} | Rating: ${movie.rating}
</div>
);
};
setStoreMe
Updating the global state
In the following examples we didn't consume the "isMenuExpanded" value with useStoreMe because it's not required for the component to have the state if we just need to update it.
import { setStoreMe } from "store-me";
const App = () => {
return (
<>
// Static value update
<div onClick={() => setStoreMe({ isMenuExpanded: true })}>
Expand Menu
</div>
// Static value update
<div onClick={() => setStoreMe({ isMenuExpanded: false })}>
Collapse Menu
</div>
// Dynamic value update
<div
onClick={() => {
setStoreMe(({ isMenuExpanded }) => ({
isMenuExpanded: !isMenuExpanded,
}));
}}
>
Toggle Menu
</div>
// Dynamic value update with complexity
<div
onClick={() => {
setStoreMe(globalState => {
const { user } = globalState;
return { isMenuExpanded: user && user.age > 18 ? true : false };
});
}}
>
Open Menu if user is logged in and more than 18 years old.
</div>
</>
);
};
getStoreMe
Retrieve state on demand
It's not required to be subscribed to a specific state value in order to access it. getStoreMe can be used in various scenarios as you will see in the examples listed here.
In this situation, we want to send some data to the backend on click event. Until the user initiate that click there is no point of having re-renders when the user object or the language changes. We can consume the state just when we want it.
import { getStoreMe } from "store-me";
const App = () => {
return (
<div
onClick={() => {
const { user, language } = getStoreMe("user", "language");
saveEventInTheDataBase(user.name, language);
}}
>
I want to proceed!
</div>
);
};
resetStoreMe
Reset values to their original state
It could be useful to reset some state with initial values, let's say when the user logs in or out. There are three different reset options. For convenience let's show them in one place:
import { useStoreMe, resetStoreMe } from "store-me";
import { useEffect } from "react";
const App = () => {
const { user } = useStoreMe("user");
useEffect(() => {
if (!user) {
/* Option 1. Reset specific values to their initial state when they were created */
resetStoreMe("language", "isMenuExpanded");
/* Option 2. Reset all values to their initial state when they were created */
resetStoreMe();
/* Option 3. Reset the entire state with the initial one received when initializing StoreMe component */
resetStoreMe("initial-store-me");
}
}, [user]);
return <div>{user ? `Hello, ${user.name}` : "Click here to log in."}</div>;
};
deleteStoreMe
Delete values from the state
In specific cases it could be useful to delete some state.
import { useStoreMe, deleteStoreMe } from "store-me";
import { useEffect } from "react";
// Current global state:
const globalState = {
movies_id: ["fpl-352-hmduwfi-98352-4938", "pln-653-jfeughw-64837-2645"],
"fpl-352-hmduwfi-98352-4938": {
name: "Batman",
year: 1989,
rating: 7.5,
},
"pln-653-jfeughw-64837-2645": {
name: "The Matrix",
year: 1999,
rating: 8.7,
},
};
const App = () => {
const { movies_id } = useStoreMe("movies_id");
useEffect(() => {
if (!user) {
deleteStoreMe("movies_id", ...movies_id);
}
}, [user]);
return <div>{user ? `Hello, ${user.name}` : "Click here to log in."}</div>;
};
renderStoreMe
Update React UI on demand
You will need this only if you use the second argument of setStoreMe
An example can be seen in the Performance recipes
Performance recepies
storeMeSubscriber
Manually subscribe for state updates
Depending on the type of your application you may need a way to fully control component updates. This is where the "storeMeSubscriber" comes into play. Let's see some possible usages and cases.
Example 1.
The goal is to update the component only when it's visible on the screen. This is useful for cases when frequently updated components are not always visible because of a scroll. Imagine 200 components in a scrollable list, showing the user profit changing 10 times per second.
import { getStoreMe, storeMeSubscriber } from "store-me";
import { useState, useEffect } from "react";
import VisibilitySensor from "react-visibility-sensor";
const subscriptionKeys = ["user", "language"];
const App = () => {
const [state, setState] = useState(getStoreMe(subscriptionKeys));
const [isVisible, setIsVisible] = useState(true);
const { user, language } = state;
/*
Every time "isVisible" becomes true,
the component will subscribe to any updates on the user and language values.
If "isVisible" becomes false, the return function will unsubscribe.
The reason for calling the "setState" when "isVisible" becomes true is to set
the latest values from the global state to our local component state.
Keep in mind that "storeMeSubscriber" does not retrieve the state upon initialization,
only when an actual update occurs.
*/
useEffect(() => {
if (isVisible) {
const subscription = storeMeSubscriber(subscriptionKeys, setState);
setState(getStoreMe(subscriptionKeys));
return () => subscription();
}
}, [isVisible, storeMeSubscriber]);
return (
<VisibilitySensor onChange={setIsVisible}>
<div>
Hello {user.name}. Your profit is ${user.profit}
</div>
</VisibilitySensor>
);
};
Example 2.
We are going to do the same thing with a different approach, just to show how much control you can have.
import { getStoreMe, storeMeSubscriber } from "store-me";
import { useState, useEffect, useRef } from "react";
import VisibilitySensor from "react-visibility-sensor";
const subscriptionKeys = ["user", "language"];
const App = () => {
const [state, setState] = useState(getStoreMe(subscriptionKeys));
const isVisibleRef = useRef(true);
const { user, language } = state;
/*
Instead of using "isVisible" state and subscribe/unsubscribe depending on it's value,
we could be always subscribed but update our component state only when we want, using
the React useRef to determine if we want or not.
This is useful if you have multiple conditions
upon which you decide if you want to update or not.
*/
useEffect(() => {
const subscription = storeMeSubscriber(subscriptionKeys, data => {
isVisible.current && setState(data);
});
return () => subscription();
}, [storeMeSubscriber]);
return (
<VisibilitySensor
onChange={isVisible => {
isVisibleRef.current = isVisible;
}}
>
<div>
Hello {user.name}. Your profit is ${user.profit}
</div>
</VisibilitySensor>
);
};
Example 3.
Controlling update frequency. Let's say the user profit is updated from the backend 10 times per second. This data is going into your global state but you want to display the changes every 5 seconds.
import { getStoreMe, storeMeSubscriber } from "store-me";
import { useState, useEffect, useRef } from "react";
const subscriptionKeys = ["user", "language"];
const App = () => {
const [state, setState] = useState(getStoreMe(subscriptionKeys));
const { user, language } = state;
useEffect(() => {
let lastUpdateTime = 0;
let latestState = "no_updates_yet";
const subscription = storeMeSubscriber(subscriptionKeys, data => {
latestState = data;
});
const initAnimationFrame = (time = 0) => {
requestAnimationFrame(initAnimationFrame);
if (time - lastUpdateTime >= 5000) {
lastUpdateTime = time;
latestState !== "no_updates_yet" && setState(latestState);
}
};
initAnimationFrame();
return () => subscription();
}, [storeMeSubscriber]);
return (
<div>
Hello {user.name}. Your profit is ${user.profit}
</div>
);
};
useStoreMe
Imagine that your application receives big list of movies to present. After receiving the initial list you will continue receive live and frequent updates for their ratings. If we have an array of 100 movie objects, then we update two of them, we will change the entire list, thus forcing React to re-render it.
One possible solution is to write each movie as a separate value in the global state and keep one array of all movie IDs. Then we will render the list once using the array of IDs and subscribe each movie independently. Later on if you need to add new movie in the list or sort the movies by some criteria, you will work with the array of IDs and cause re-render of the entire list only in those situations.
// Current global state:
const globalState = {
movies_id: ["fpl-352-hmduwfi-98352-4938", "pln-653-jfeughw-64837-2645"],
"fpl-352-hmduwfi-98352-4938": {
name: "Batman",
year: 1989,
rating: 7.5,
},
"pln-653-jfeughw-64837-2645": {
name: "The Matrix",
year: 1999,
rating: 8.7,
},
};
const App = () => {
const { movies_id } = useStoreMe("movies_id");
return movies_id.map(id => <Movie movie_id={id} />);
};
const Movie = movie_id => {
const { [movie_id]: movie } = useStoreMe(movie_id);
return (
<div>
Movie {movie.name} - {movie.year} | Rating: ${movie.rating}
</div>
);
};
setStoreMe and renderStoreMe
Image that your application receives stream of data trough webSockets, several times per second. Multiple channels are sending different data which you want to display at once every 5 seconds. Combining the second argument of setStoreMe and manually render specific UI changes can achieve exactly this.
// Current global state:
const globalState = {
// Constantly updated 4 times per second
movies_ratings: [
{ id: 1, rating: 7.5 },
{ id: 2, rating: 9 },
],
// Constantly updated 2 times per second
profits_by_rating: [
{ id: 1, profit: 760040 },
{ id: 2, profit: 1260040 },
],
// Constantly updated 5 times per second
comments: [
{ id: 1, text: "I love this movie..." },
{ id: 2, text: "I've seen better..." },
],
};
const App = () => {
useEffect(() => {
subscribeForMovieRatings(movies_ratings => {
setStoreMe({ movies_ratings }, true);
});
subscribeForMovieProfits(profits_by_rating => {
setStoreMe({ profits_by_rating }, true);
});
subscribeForMovieComments(comments => {
setStoreMe({ comments }, true);
});
}, []);
return <Consumer />;
};
const Consumer = () => {
const { movies_ratings, profits_by_rating, comments } = useStoreMe(
"movies_ratings",
"profits_by_rating",
"comments"
);
useEffect(() => {
let lastUpdateTime = 0;
const initAnimationFrame = (time = 0) => {
requestAnimationFrame(initAnimationFrame);
if (time - lastUpdateTime >= 5000) {
lastUpdateTime = time;
renderStoreMe("movies_ratings", "profits_by_rating", "comments");
}
};
initAnimationFrame();
}, []);
return (
<div>
Ratings: {movies_ratings.map(({ rating }) => rating)} <br />
Profits: {profits_by_rating.map(({ profit }) => profit)} <br />
Comments: {comments.map(({ text }) => text)} <br />
</div>
);
};
API
StoreMe
Example
Type: Component
Arguments:
initialState
of type object{}
syncStateKeys
of type array[]
Example:<StoreMe syncStateKeys={["language", "theme"]}>
It makes sense for some parts of the state to be shared between multiple browser tabs or windows.
For example if user changes the color theme from one tab, you would like to switch the theme in all opened tabs.
Attention! Use this feature wisely since you could cause yourself unintentional bugs.
Imagine that some particular state with initial value offalse
will trigger a REST call to your backend if the value becomestrue
.
You probably don't want to make this call 5 times (in case the user has your application opened in 5 browser tabs).
This is why you must be careful what part of the state you sync between tabs and why this feature is opt-in.debug
acceptsarray
withnumber
values. Example:<StoreMe debug={[1, 2, 3]}>
Currently there are 3 logs which you can see independently by specifying their ID in the array.- 1 - Will show you how much time was needed for React to render the components which were affected of the last state change.
This could help you to find components that take too much time to render and optimize them.
Example output:[1] React updated and rendered 005 components for 10.8799999980 ms [Array(5)]
- 2 - Will show you the time needed for StoreMe to check the new state differences and decide which component should be updated.
Example output:[2] StoreMe built components state for 1.8199999976786785 ms
- 3 - Will show you how many components are currently connected to StoreMe.
Example output:[3] Current StoreMe connected components: 2672
- 1 - Will show you how much time was needed for React to render the components which were affected of the last state change.
This could help you to find components that take too much time to render and optimize them.
Returns: children
prop
useStoreMe
Example
Type: Hook/Function
Arguments: string
or number
. Single or multiple separated by comma. The are three types of accessors.
- Single, for example
user
. Accessing any type of value. - Nested
user.settings.app.theme
. Accessing nested property in object. It will always return object with the specified in the accessor structure even if "user" doesn't exist in the global state. - Nested multiple
user.settings.app.[theme|version|type]
Accessing nested properties in object. It will always return object with the specified in the accessor structure even if "user" doesn't exist in the global state.
setStoreMe
Example
Type: Function
Arguments:
object
with single or multiple values.function
which should return an object. By using the function you will receive the entire state as an argument to it. This is the same as using the ReactsetState(prevCount => prevCount + 1)
There is a second possible boolean
argument which by default is set to false
. If passed as true, "StoreMe" will not render those updates to the UI.
This option should be used with caution. Example usage can be seen here\
getStoreMe
Example
Type: Function
Arguments: The same as useStoreMe. The are two differences with the useStoreMe method.
- It is not automatic subscription to the global state. It's rather an on demand data fetcher from the state.
- It will always fetch the state values, while useStoreMe is returning them when the value is changed.
resetStoreMe
Example
Type: Function
Arguments:
string/s
ornumber/s
representing a key/s in the global state. "Store me" will reset the state of the specified keys to their initial value.- If no argument is passed, "Store me" will reset all state values to their initial state when they were created.
- If you pass "initial-store-me" "Store me" will reset the entire state to the initial one received when initializing StoreMe component
deleteStoreMe
Example
Type: Function
Arguments:
string/s
ornumber/s
representing a key/s in the global state. "Store me" will delete these values from the state as they never were.
renderStoreMe
Example
Type: Function
Arguments:
string/s
ornumber/s
representing a key/s in the global state. "Store me" will re-render the UI for the specified accessors (if they were not rendered already).
storeMeSubscriber
Example
Type: Function
Arguments:
array
with the same type of accessors as described in useStoreMefunction
which will be executed every time there is an update to the values you are subscribed for.
Returns: function
which will unsubscribe from "Store me" updates when the component is destroyed (unmounted).
You if you don't clean up your subscription you will cause yourself a memory leak.