react-easier / spreado

Easier to spread things across React components

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Spreado

Easier to spread things across React components

Why

It's common to use a combination of a state managing lib (like Redux) and a data fetching lib (like 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

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:

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<ReturnType<typeof useSomeDataQuerySpreadOut>>(INDEX_OF_SOME_DATA_QUERY, {});
}

const ComponentA: FC = () => {
  const params = prepare_params_for_fetching_some_data();
  const {isLoading, isSuccess, data, refetch} = useSomeDataQuerySpreadOut(params);
  return (
    <div>
      {isLoading && <Loading />}
      {isSuccess && <ResultA data={data} />}
      <button onClick={() => refetch()}>Refresh data</button>
    </div>
  );
};

const ComponentB: FC = () => {
  const {isLoading, isSuccess, data} = useSomeDataQuerySpreadIn();
  return (
    <div>
      {isLoading && <Loading />}
      {isSuccess && <ResultB data={data} />}
    </div>
  );
};

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, MobX, React Query, SWR) have been supported by spreado. You may pick up your preferred pair, then setup spreado as follows:

For Redux and React Query

// 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 (
    <ReduxProvider store={store}>
      <QueryClientProvider client={queryClient}>
        <SpreadoSetupProvider setup={spreadoSetup}>
          <div>...</div>
        </SpreadoSetupProvider>
      </QueryClientProvider>
    </ReduxProvider>
  );
};

For Redux Toolkit and React Query

// 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 (
    <ReduxProvider store={store}>
      <QueryClientProvider client={queryClient}>
        <SpreadoSetupProvider setup={spreadoSetup}>
          <div>...</div>
        </SpreadoSetupProvider>
      </QueryClientProvider>
    </ReduxProvider>
  );
};

For Redux and SWR

// 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 (
    <ReduxProvider store={store}>
      <SpreadoSetupProvider setup={spreadoSetup}>
        <div>...</div>
      </SpreadoSetupProvider>
    </ReduxProvider>
  );
};

For Redux Toolkit and SWR

// 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 (
    <ReduxProvider store={store}>
      <SpreadoSetupProvider setup={spreadoSetup}>
        <div>...</div>
      </SpreadoSetupProvider>
    </ReduxProvider>
  );
};

For MobX and React Query

// 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 (
    <QueryClientProvider client={queryClient}>
      <SpreadoSetupProvider setup={spreadoSetup}>
        <div>...</div>
      </SpreadoSetupProvider>
    </QueryClientProvider>
  );
};

For MobX and SWR

// 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 (
    <SpreadoSetupProvider setup={spreadoSetup}>
      <div>...</div>
    </SpreadoSetupProvider>
  );
};

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:

import {setSpreadOut, useSpreadIn} from 'spreado';

const INDEX_OF_IS_SOMETHING_VISIBLE = 'INDEX_OF_IS_SOMETHING_VISIBLE';

function useIsSomethingVisible() {
  return useSpreadIn<boolean>(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 (
    <div>
      {isSomethingVisible && <div>Part A related to something</div>}
      <button onClick={() => setIsSomethingVisible(true)}>Show</button>
      <button onClick={() => setIsSomethingVisible(false)}>Hide</button>
      <div>Everything else in component A</div>
    </div>
  );
};

const ComponentB: FC = () => {
  const isSomethingVisible = useIsSomethingVisible();
  return (
    <div>
      {isSomethingVisible && <div>Part B related to something</div>}
      <div>Everything else in component B</div>
    </div>
  );
};

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:

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(
    <ReduxProvider store={store}>
      <QueryClientProvider client={queryClient}>
        <SpreadoSetupProvider setup={spreadoSetup}>
          <div>...</div>
        </SpreadoSetupProvider>
      </QueryClientProvider>
    </ReduxProvider>
  );

  res.send(
    `...
<script>window.INITIAL_GLOBAL_STATE='${JSON.stringify(initialGlobalState)}';</script>
<div id="app">${htmlContent}</div>
    ...`
  );
});

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:

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 (
    <ReduxProvider store={store}>
      <QueryClientProvider client={queryClient}>
        <SpreadoSetupProvider setup={spreadoSetup}>
          <div>...</div>
        </SpreadoSetupProvider>
      </QueryClientProvider>
    </ReduxProvider>
  );
};

hydrate(<App>, 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:

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:

API

Here are full descriptions for core APIs of spreado. Please have a look as needed:

useSpreadOut

useSpreadOut<T>(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

useSpreadIn<T>(index: unknown): T | undefined;
useSpreadIn<T>(index: unknown, fallback: Partial<T>): T | Partial<T>;
useSpreadIn<T>(index: unknown, fallback?: Partial<T>): T | Partial<T> | 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

setSpreadOut<T>(index: unknown, value: T): T;
setSpreadOut<T>(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

getSpreadIn<T>(index: unknown): T | undefined;
getSpreadIn<T>(index: unknown, fallback: Partial<T>): T | Partial<T>;
getSpreadIn<T>(index: unknown, fallback?: Partial<T>): T | Partial<T> | 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):


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 specification. Contributions of any kind welcome!

About

Easier to spread things across React components

License:MIT License


Languages

Language:TypeScript 99.3%Language:JavaScript 0.7%