swr-global-state v2.2.0
♻️ SWR Global State
Zero-setup & simple global state management for React Components based on SWR helpers. With this library, you can focus on your awesome React Project and not waste another afternoon on the setup & configuring your global state. 🌄
Table of Contents
- ♻️ SWR Global State
- Table of Contents
- Getting Started
- Demo
- FAQ
- Publishing
- License
- Feedbacks and Issues
- Support
Getting Started
Install
NPM
npm i swr swr-global-stateYarn
yarn add swr swr-global-stateUsage
Creating a Store
Create a new file for your global state on your root directory. And then, use createStore. Example: stores/counter.js
// file: stores/counter.js
import { createStore } from "swr-global-state";
const useCounter = createStore({
key: "@app/counter", // (Required) state key with unique string
initial: 0 // <- (Required) initial state
});
export default useCounter;Using store on your component
You just import stores that you have created into your any components, then use it like you use useState as usual.
// file: components/SetCountComponent.js
import useCounter from "stores/counter";
function SetCountComponent() {
const [, setCount] = useCounter(); // <- `[, ]` skipping first index of the array.
return (
<div>
<button onClick={() => setCount(prev => prev - 1)}>
(-) Decrease Count
</button>
<button onClick={() => setCount(prev => prev + 1)}>
(+) Increase Count
</button>
</div>
);
}
export default SetCountComponent;// file: components/GetCountComponent.js
import useCounter from "stores/counter";
function GetCountComponent() {
const [count] = useCounter();
return (
<div>
<p>Current Count: {count}</p>
</div>
);
}
export default GetCountComponent;Persisted State
Creating Persisted State
Optionally, you can define persistor object to create custom persistor to hold your state even user has closing app/browser, and re-opened it.
In this example, we use localStorage to hold our state.
// file: stores/counter.js
import { createStore } from "swr-global-state";
const useCounter = createStore({
key: "@app/counter",
initial: 0,
persistor: { // <- Optional, use this if you want hold the state
onSet: (key, data) => {
window.localStorage.setItem(String(key), data);
},
onGet: (key) => {
const cachedData = window.localStorage.getItem(String(key));
return Number(cachedData);
}
}
});
export default useCounter;Reusable Persistor (Example in TypeScript)
We can create reusable persistor to re-use in every stores that we have created. Example:
// file: persistors/local-storage.ts
import type { StatePersistor, StateKey } from "swr-global-state";
const withLocalStoragePersistor = <T = any>(): StatePersistor<T> => ({
onSet(key: StateKey, data: T) {
const stringifyData = JSON.stringify(data);
window.localStorage.setItem(String(key), stringifyData);
},
onGet(key: StateKey) {
const cachedData = window.localStorage.getItem(String(key)) ?? "null";
try {
return JSON.parse(cachedData);
} catch {
return cachedData;
}
}
});
export default withLocalStoragePersistor;Now, we can use that withLocalStoragePersistor in that like this:
// file: stores/counter.ts
import { createStore } from "swr-global-state";
import withLocalStoragePersistor from "persistors/local-storage";
const useCounter = createStore<number>({
key: "@app/counter",
initial: 0,
persistor: withLocalStoragePersistor()
});
export default useCounter;// file: stores/theme.ts
import { createStore } from "swr-global-state";
import withLocalStoragePersistor from "persistors/local-storage";
const useTheme = createStore<string>({
key: "@app/theme",
initial: "light",
persistor: withLocalStoragePersistor()
});
export default useTheme;Asynchronous Persistor
Just use async function or promise as usual in onSet and onGet.
// file: stores/counter.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createStore } from "swr-global-state";
const useCounter = createStore({
key: "@app/counter",
initial: 0,
persistor: {
async onSet(key, data) {
try {
await AsyncStorage.setItem(String(key), data);
} catch (err) {
// handle saving error, default throw an error
throw new Error(err);
}
},
async onGet(key) {
try {
const value = await AsyncStorage.getItem(String(key));
return Number(value);
} catch (err) {
// handle error reading value
throw new Error(err);
}
}
}
});
export default useCounter;Best Practice with Persistor
Best practice in using persistor is use Debouncing Technique. This example is using debouncing in onSet callback. So, it will not spamming to call the callback request every state changes.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createStore } from "swr-global-state";
const withDebounce = (fn, time) => {
let timeoutId;
const wrapper = (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = null;
fn(...args);
}, time);
};
return wrapper;
};
const useUser = createStore({
key: "@app/user",
initial: null,
persistor: {
onSet: withDebounce(async(key, user) => {
try {
const stringifyUser = JSON.stringify(user)
await AsyncStorage.setItem(String(key), stringifyUser);
} catch (err) {
// handle saving error, default throw an error
throw new Error(err);
}
}, 1000), // debounce-effect in 1 second.
...
}
});
export default useUser;Custom hooks
Can't find your cases in this documentation examples? You can create custom hooks by yourself. Here is complex example you can refer the pattern to create another custom hooks cases.
// file: stores/account.js
...
import useStore from "swr-global-state";
const KEY = "@app/account";
function useAccount() {
const [loading, setLoading] = useStore({
key: `${KEY}-loading`,
initial: true
});
const [account, setAccount, swrDefaultResponse] = useStore(
{
key: KEY,
initial: null,
persistor: {
onSet: (key, accountData) => {
window.localStorage.setItem(String(key), JSON.stringify(accountData));
},
onGet: async(key) => {
if (window.navigator.onLine) {
const remoteAccount = await fetch('/api/account');
return remoteAccount.json();
}
const cachedAccount = window.localStorage.getItem(String(key));
setLoading(false);
return JSON.parse(cachedAccount);
}
}
},
{
/**
* set another SWR config here
* @see https://swr.vercel.app/docs/options#options
* @default on `swr-global-state`:
* revalidateOnFocus: false
* revalidateOnReconnect: false
* refreshWhenHidden: false
* refreshWhenOffline: false
*/
revalidateOnFocus: true,
revalidateOnReconnect: true
}
);
/**
* Destructuring response from SWR Default response
* @see https://swr.vercel.app/docs/options#return-values
*/
const { mutate, error } = swrDefaultResponse;
const destroyAccount = async () => {
setLoading(true);
await fetch('/api/account/logout');
window.localStorage.removeItem(KEY);
// use default `mutate` from SWR to avoid `onSet` callback in `persistor`
mutate(null);
setLoading(false);
};
const updateAccount = async (newAccountData) => {
setLoading(true);
await fetch('/api/account', {
method: 'POST',
body: JSON.stringify(newAccountData)
...
})
setAccount(newAccountData);
setLoading(false);
};
// your very custom mutator/dispatcher
return {
loading,
error,
account,
updateAccount,
destroyAccount
};
}
export default useAccount;Then, use that custom hooks in your component as usual.
// file: App.js
...
import useAccount from "stores/account";
function App() {
const {
account,
updateAccount,
destroyAccount,
loading,
error
} = useAccount();
const onLogout = async () => {
await destroyAccount()
// your very logic
}
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>An Error occured</div>;
}
return (
<div>
<p>Account Detail: {JSON.stringify(account)}</p>
<button onClick={onLogout}>Logout</button>
{/* your very component to update account */}
</div>
);
}
export default App;Demo
You can see live demo here
FAQ
Why should I use this?
- If you want to manage your global state like
useStateas usual. - If you want to manage your global state without involving in setting up Provider Component, Dispatcher, Reducer, etc.
- If you want to see
ReduxorContext APIalternative. - If you're already use
SWR, but you have no idea how to manage synchronous global state withSWRon client-side. - If you're still use
ReduxorContext API, but you are overwhelmed with their flow.
If this library can cover Redux, how about asynchronous state management like redux-saga, redux-thunk, or redux-promise?
At this point, swr-global-state is based and depends on SWR. After version >2 or later, swr-global-state now can handle async state too. Just wraps your very async state logic into a function like in Custom Hooks or Asynchronous Persistor.
So, you basically don't need to use Redux or Context API anymore. Alternatively, you can choose TanStack Query or default SWR itself.
React Native
Since SWR itself supports React Native, of course swr-global-state supports it too. This example is using Async Storage in React Native.
Things to note, you must install swr-global-state version >2 or later, because it has customizable persistor. So, you can customize the persistor with React Native Async Storage.
Under version <2, swr-global-state still use localStorage and we can't customize it. So, it doesn't support React Native.
Publishing
- Before pushing your changes to Github, make sure that
versioninpackage.jsonis changed to newest version. Then runnpm installfor synchronize it topackage-lock.json - After your changes have been merged on branch
main, you can publish the packages by creating new Relase here: https://github.com/gadingnst/swr-global-state/releases/new - Create new
tag, make sure thetagname is same as theversioninpackage.json. - You can write Release title and notes here. Or you can use auto-generated release title and notes.
- Click
Publish Releasebutton, then wait the package to be published.
License
swr-global-state is freely distributable under the terms of the MIT license.
Feedbacks and Issues
Feel free to open issues if you found any feedback or issues on swr-global-state. And feel free if you want to contribute too! 😄
Support
Global
Indonesia
Built with ❤️ by Sutan Gading Fadhillah Nasution on 2022
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago