https://github.com/react-easier/spreado
Easier to spread things across React components
https://github.com/react-easier/spreado
mobx react react-easier react-query redux redux-toolkit share spread spreado swr
Last synced: 6 months ago
JSON representation
Easier to spread things across React components
- Host: GitHub
- URL: https://github.com/react-easier/spreado
- Owner: react-easier
- License: mit
- Created: 2022-01-18T08:50:02.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2022-12-05T08:06:00.000Z (over 3 years ago)
- Last Synced: 2023-12-26T07:08:16.975Z (over 2 years ago)
- Topics: mobx, react, react-easier, react-query, redux, redux-toolkit, share, spread, spreado, swr
- Language: TypeScript
- Homepage:
- Size: 107 KB
- Stars: 28
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Spreado
[](https://app.codecov.io/gh/react-easier/spreado)
[](https://www.npmjs.com/package/spreado)
[](https://github.com/react-easier/spreado/blob/main/LICENSE)
[](https://github.com/semantic-release/semantic-release)
> Easier to spread things across React components
## Why
It's common to use a combination of a state managing lib (like [Redux](https://github.com/reduxjs/redux)) and a data fetching lib (like [React Query](https://github.com/tannerlinsley/react-query)) in the development of a React app. But spreading a data fetching result from a React component to another is tedious. The work involves mapping the data fetching result to a timely-updated redux state so to share the access beyond the current React component.
Spreado provides a set of intuitive APIs to simplify that kind of tedious work for you, making spreading things easily across React components. The combinations of well-known state managing libs and data fetching libs are supported and those supported libs are only regarded as peer dependencies. The bonus is, any usages of peer dependencies are kept still available.
## Install
```sh
npm install spreado
```
## Usage
### Spread a result of data fetching
Supposing the `ComponentA` prepares params and fetches data with those params using the `useQuery` from React Query (or using the `useSWR` from SWR), and now we want to get the data fetching result visited in the `ComponentB`, the implementation with Spreado looks like:
```tsx
import {
useQuery, // or useSWR from `swr`
} from 'react-query';
import {useSpreadIn, useSpreadOut} from 'spreado';
const INDEX_OF_SOME_DATA_QUERY = 'INDEX_OF_SOME_DATA_QUERY';
function useSomeDataQuerySpreadOut(params: ParamsForSomeDataQuery) {
return useSpreadOut(
INDEX_OF_SOME_DATA_QUERY,
useQuery([INDEX_OF_SOME_DATA_QUERY, params], () => fetch_some_data_with_params(params))
);
}
function useSomeDataQuerySpreadIn() {
return useSpreadIn>(INDEX_OF_SOME_DATA_QUERY, {});
}
const ComponentA: FC = () => {
const params = prepare_params_for_fetching_some_data();
const {isLoading, isSuccess, data, refetch} = useSomeDataQuerySpreadOut(params);
return (
{isLoading && }
{isSuccess && }
refetch()}>Refresh data
);
};
const ComponentB: FC = () => {
const {isLoading, isSuccess, data} = useSomeDataQuerySpreadIn();
return (
{isLoading && }
{isSuccess && }
);
};
```
The snake-case named functions are placeholders. The data fetching result gets spread by `useSomeDataQuerySpreadOut` in `ComponentA` and gets visited by `useSomeDataQuerySpreadIn` in `ComponentB`. The second param of `useSpreadIn` is a fallback value in case the spread value is not available. The data fetching result stays timely-updated in `ComponentB` even if the data fetching helper `refetch` is invoked in `ComponentA`.
### Initialization
Spreado expects a pair of a state managing lib and a data fetching lib has been adopted by your React app. It aims to integrate with them well but won't re-invent wheels itself. Most well-known libs of the categories (e.g. [Redux](https://github.com/reduxjs/redux), [MobX](https://github.com/mobxjs/mobx), [React Query](https://github.com/tannerlinsley/react-query), [SWR](https://github.com/vercel/swr)) have been supported by spreado. You may pick up your preferred pair, then setup spreado as follows:
#### For Redux and React Query
```tsx
// Requires peer dependencies installed: `react`, `redux`, `react-redux`, `react-query`.
import React, {FC} from 'react';
import {QueryClient, QueryClientProvider} from 'react-query';
import {Provider as ReduxProvider} from 'react-redux';
import {combineReducers, createStore} from 'redux';
import {SpreadoSetupProvider} from 'spreado';
import {
spreadoReduxReducerPack,
SpreadoSetupForReduxReactQuery,
} from 'spreado/for-redux-react-query';
const store = createStore(combineReducers(spreadoReduxReducerPack));
const queryClient = new QueryClient();
const spreadoSetup = new SpreadoSetupForReduxReactQuery({store, queryClient});
const App: FC = () => {
return (
...
);
};
```
#### For Redux Toolkit and React Query
```tsx
// Requires peer dependencies installed: `react`, `@reduxjs/toolkit`, `react-redux`, `react-query`.
import {configureStore} from '@reduxjs/toolkit';
import React, {FC} from 'react';
import {QueryClient, QueryClientProvider} from 'react-query';
import {Provider as ReduxProvider} from 'react-redux';
import {SpreadoSetupProvider} from 'spreado';
import {
spreadoReduxReducerPack,
SpreadoSetupForReduxReactQuery,
} from 'spreado/for-redux-react-query';
const store = configureStore({
reducer: spreadoReduxReducerPack,
middleware: (m) => m({serializableCheck: false}),
});
const queryClient = new QueryClient();
const spreadoSetup = new SpreadoSetupForReduxReactQuery({store, queryClient});
const App: FC = () => {
return (
...
);
};
```
#### For Redux and SWR
```tsx
// Requires peer dependencies installed: `react`, `redux`, `react-redux`, `swr`.
import React, {FC} from 'react';
import {Provider as ReduxProvider} from 'react-redux';
import {combineReducers, createStore} from 'redux';
import {SpreadoSetupProvider} from 'spreado';
import {spreadoReduxReducerPack, SpreadoSetupForReduxSwr} from 'spreado/for-redux-swr';
const store = createStore(combineReducers(spreadoReduxReducerPack));
const spreadoSetup = new SpreadoSetupForReduxSwr({store});
const App: FC = () => {
return (
...
);
};
```
#### For Redux Toolkit and SWR
```tsx
// Requires peer dependencies installed: `react`, `@reduxjs/toolkit`, `react-redux`, `swr`.
import {configureStore} from '@reduxjs/toolkit';
import React, {FC} from 'react';
import {Provider as ReduxProvider} from 'react-redux';
import {SpreadoSetupProvider} from 'spreado';
import {spreadoReduxReducerPack, SpreadoSetupForReduxSwr} from 'spreado/for-redux-swr';
const store = configureStore({
reducer: spreadoReduxReducerPack,
middleware: (m) => m({serializableCheck: false}),
});
const spreadoSetup = new SpreadoSetupForReduxSwr({store});
const App: FC = () => {
return (
...
);
};
```
#### For MobX and React Query
```tsx
// Requires peer dependencies installed: `react`, `mobx`, `react-query`.
import React, {FC} from 'react';
import {QueryClient, QueryClientProvider} from 'react-query';
import {SpreadoSetupProvider} from 'spreado';
import {SpreadoSetupForMobXReactQuery} from 'spreado/for-mobx-react-query';
const queryClient = new QueryClient();
const spreadoSetup = new SpreadoSetupForMobXReactQuery({queryClient});
const App: FC = () => {
return (
...
);
};
```
#### For MobX and SWR
```tsx
// Requires peer dependencies installed: `react`, `mobx`, `swr`.
import React, {FC} from 'react';
import {SpreadoSetupProvider} from 'spreado';
import {SpreadoSetupForMobXSwr} from 'spreado/for-mobx-swr';
import {SWRConfig} from 'swr';
const spreadoSetup = new SpreadoSetupForMobXSwr();
const App: FC = () => {
return (
...
);
};
```
Notice that constructors `SpreadoSetupForMobX...` optionally accept a param `store` that can get instantiated by `new SpreadoMobXStore()`. If the `store` is give, constructors `SpreadoSetupForMobX...` will use it. Otherwise, they will create one internally.
### Maintain simple global states
Another potential usage of Spreado is to maintain simple global states. State managing libs are definitely able to maintain global state because that's just what they are created for. But when it comes to simple global states (like a boolean state), with Spreado we can maintain them more easily. Meanwhile, although Spreado builds its APIs based on the state managing lib in the React app, it only regards it as a peer dependency. That is, when it comes to complex global states (like some very complex array), the peer-dependency state managing lib is still available for us.
Assuming there is a boolean global state that 2 components share, the implementation with Spreado looks like:
```tsx
import {setSpreadOut, useSpreadIn} from 'spreado';
const INDEX_OF_IS_SOMETHING_VISIBLE = 'INDEX_OF_IS_SOMETHING_VISIBLE';
function useIsSomethingVisible() {
return useSpreadIn(INDEX_OF_IS_SOMETHING_VISIBLE, false);
}
function setIsSomethingVisible(v: boolean) {
return setSpreadOut(INDEX_OF_IS_SOMETHING_VISIBLE, v);
}
const ComponentA: FC = () => {
const isSomethingVisible = useIsSomethingVisible();
return (
{isSomethingVisible && Part A related to something}
setIsSomethingVisible(true)}>Show
setIsSomethingVisible(false)}>Hide
Everything else in component A
);
};
const ComponentB: FC = () => {
const isSomethingVisible = useIsSomethingVisible();
return (
{isSomethingVisible && Part B related to something}
Everything else in component B
);
};
```
The boolean global state `isSomethingVisible` is managed by the pair of functions `useIsSomethingVisible` and `setIsSomethingVisible` which read and write the value. The initial value of the state is specified by the second param of `useSpreadIn`.
### Sever side rendering (SSR)
SSR process of a modern React app works like this in general:
1. When a http request for a html page hits the server side, the server side prepares data according to the http request, then the data are used to produce the initial global state of the root client side React component to render the html page. Meanwhile, the initial global state is serialized together with the html page.
2. When a requested html page arrives in the client side, if the client side is a regular browser, the client side deserializes the initial global state and uses it together with the root client side React component to hydrate to initialize the React app. If the client side is a web crawler with JavaScript disabled, the html content remains available at least.
Spreado follows that pattern. In the server side, spreado provides helpers for producing the initial global state. Let's take an example by continuing the usage section _Spread a result of data fetching_ and the spreado setup for `redux` and `react-query`:
```tsx
import {INDEX_OF_SOME_DATA_QUERY} from '@/client';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {QueryClient, QueryClientProvider} from 'react-query';
import {Provider as ReduxProvider} from 'react-redux';
import {combineReducers, createStore} from 'redux';
import {SpreadoSetupProvider} from 'spreado';
import {
createSpreadoReduxPreloadedState,
renderQueryResult,
spreadoReduxReducerPack,
SpreadoSetupForReduxReactQuery,
} from 'spreado/for-redux-react-query';
app.get('/some-page', (req, res) => {
const someData = prepare_some_data_according_to_the_http_request(req);
const initialGlobalState = createSpreadoReduxPreloadedState({
[INDEX_OF_SOME_DATA_QUERY]: renderQueryResult(someData),
});
const store = createStore(combineReducers(spreadoReduxReducerPack), initialGlobalState);
const queryClient = new QueryClient();
const spreadoSetup = new SpreadoSetupForReduxReactQuery({store, queryClient});
const htmlContent = renderToString(
...
);
res.send(
`...
window.INITIAL_GLOBAL_STATE='${JSON.stringify(initialGlobalState)}';
${htmlContent}
...`
);
});
```
The `app.get` is the pseudo code for handling http requests for html pages in the server side, the snake-case named functions are placeholders, and the `...` in `res.send` is the rest required html snippets for a working html page. Then, in the client side, we deserialize the initial global state and hydrate:
```tsx
import React, {FC} from 'react';
import {hydrate} from 'react-dom';
import {QueryClient, QueryClientProvider} from 'react-query';
import {Provider as ReduxProvider} from 'react-redux';
import {combineReducers, createStore} from 'redux';
import {SpreadoSetupProvider} from 'spreado';
import {spreadoReduxReducerPack, SpreadoSetupForReduxReactQuery} from 'spreado/redux-react-query';
const store = createStore(
combineReducers(spreadoReduxReducerPack),
JSON.parse(window.INITIAL_GLOBAL_STATE)
);
const queryClient = new QueryClient();
const spreadoSetup = new SpreadoSetupForReduxReactQuery({store, queryClient});
const App: FC = () => {
return (
...
);
};
hydrate(, document.getElementById('app'));
```
After that, we set the initial data for all the `useQuery` calls of `react-query` so to have correct statuses of data fetching in the client side:
```diff
import {useSpreadIn, useSpreadOut} from 'spreado';
+import {useQueryInitialData} from 'spreado/for-redux-react-query';
const INDEX_OF_SOME_DATA_QUERY = 'INDEX_OF_SOME_DATA_QUERY';
function useSomeDataQuerySpreadOut(params: ParamsForSomeDataQuery) {
return useSpreadOut(
INDEX_OF_SOME_DATA_QUERY,
- useQuery([INDEX_OF_SOME_DATA_QUERY, params], () => fetch_some_data_with_params(params))
+ useQuery([INDEX_OF_SOME_DATA_QUERY, params], () => fetch_some_data_with_params(params), {
+ initialData: useQueryInitialData(INDEX_OF_SOME_DATA_QUERY)
+ })
);
}
```
If the client side is a regualr browser, the page will have a same look as its server side rendered html content at its initial rendering, then it will refetch the latest data immediately afterwards without entering loading states. If the client side is a web crawler with JavaScript disabled, the page just remains its server side rendered html content.
In case of using `mobx` and `swr`, similarly, you can prepare the `store` by `SpreadoMobXStore`, `createSpreadoMobXPreloadedState`, `renderSwrResponse` and set the fallback data for all the `useSWR` calls by `useSwrFallbackData`.
For more details on available SSR helpers, see also:
- [src/react-query/ssrHelpers](https://github.com/react-easier/spreado/blob/main/src/react-query/ssrHelpers.ts)
- [src/swr/ssrHelpers](https://github.com/react-easier/spreado/blob/main/src/swr/ssrHelpers.ts)
- [src/redux/ssrHelpers](https://github.com/react-easier/spreado/blob/main/src/redux/ssrHelpers.ts)
- [src/mobx/ssrHelpers](https://github.com/react-easier/spreado/blob/main/src/mobx/ssrHelpers.ts)
## API
Here are full descriptions for core APIs of spreado. Please have a look as needed:
### useSpreadOut
```ts
useSpreadOut(index: unknown, value: T): T;
```
The `useSpreadOut` is a React hook that spreads the input value by the index. It uses the integrated state managing lib to get the input value stored by the index and returns the newer version of the input value and the stored value. When the input value changes, the returned value and the stored value will change in a consistent and timely manner. When all `useSpreadOut` calls on the same index get unmounted, the stored value will be cleared alongside.
### useSpreadIn
```ts
useSpreadIn(index: unknown): T | undefined;
useSpreadIn(index: unknown, fallback: Partial): T | Partial;
useSpreadIn(index: unknown, fallback?: Partial): T | Partial | undefined;
```
The `useSpreadIn` is a React hook that reads the spread value by the index. It uses the integrated state managing lib to retrieve the stored value by the index. The stored value is returned if it's retrieved. Otherwise, `undefined` is returned. When the second param is given, this param will be returned as a fallback for the `undefined` case. When the stored value changes, the returned value will change timely.
### setSpreadOut
```ts
setSpreadOut(index: unknown, value: T): T;
setSpreadOut(index: unknown, callback: (value?: T) => T): T;
```
The `setSpreadOut` is a non-hook version of `useSpreadOut` that spreads the input value by the index. The only difference is, when the second param is a function, this param is used as a callback to accept the stored value from the integrated state managing lib and produce a new value to spread.
### getSpreadIn
```ts
getSpreadIn(index: unknown): T | undefined;
getSpreadIn(index: unknown, fallback: Partial): T | Partial;
getSpreadIn(index: unknown, fallback?: Partial): T | Partial | undefined;
```
The `getSpreadIn` is a non-hook version of `useSpreadIn` that reads the spread value by the index. No more difference.
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

Chungen Li
💻 📖 🚧 👀

Yongping Lin
💻

Donghua Zhang
💻

WeiQi Huang
💻

Le Gao
💻

Jian Liu
💻

Jian Zhu
💻

Songsong Wang
💻

Lu Lu
💻

Yu Li
💻
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!