Skip to content

Commit

Permalink
feat: Add @data-client/react/redux (#3099)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Jun 14, 2024
1 parent b79a1f6 commit 428ddd1
Show file tree
Hide file tree
Showing 49 changed files with 944 additions and 362 deletions.
2 changes: 0 additions & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
"@data-client/img",
"@data-client/normalizr",
"@data-client/react",
"@data-client/redux",
"@data-client/rest",
"@data-client/ssr",
"@data-client/test"
]
],
Expand Down
16 changes: 16 additions & 0 deletions .changeset/cool-deers-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@data-client/react': patch
---

Add [@data-client/react/redux](https://dataclient.io/docs/guides/redux)

```ts
import {
ExternalDataProvider,
PromiseifyMiddleware,
applyManager,
initialState,
createReducer,
prepareStore,
} from '@data-client/react/redux';
```
39 changes: 39 additions & 0 deletions .changeset/late-dryers-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'@data-client/react': patch
---

Add middlewares argument to prepareStore()

```tsx title="index.tsx"
import {
ExternalDataProvider,
prepareStore,
type Middleware,
} from '@data-client/react/redux';
import { getDefaultManagers, Controller } from '@data-client/react';
import ReactDOM from 'react-dom';

const managers = getDefaultManagers();
// be sure to include your other reducers here
const otherReducers = {};
const extraMiddlewares: Middleware = [];

const { store, selector, controller } = prepareStore(
initialState,
managers,
Controller,
otherReducers,
extraMiddlewares,
);

ReactDOM.render(
<ExternalDataProvider
store={store}
selector={selector}
controller={controller}
>
<App />
</ExternalDataProvider>,
document.body,
);
```
5 changes: 5 additions & 0 deletions .changeset/serious-eagles-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@data-client/redux': minor
---

Deprecate in favor of [@data-client/react/redux](https://dataclient.io/docs/guides/redux)
8 changes: 5 additions & 3 deletions docs/core/api/ExternalDataProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ in the React tree.

## Installation

<PkgTabs pkgs="@data-client/redux redux" />

## Usage

```tsx title="index.tsx"
import { ExternalDataProvider } from '@data-client/redux';
import { ExternalDataProvider } from '@data-client/react/redux';
import ReactDOM from 'react-dom';

import { store, selector } from './store';
Expand Down Expand Up @@ -58,3 +56,7 @@ but theoretically any external store could be used.
```

This function is used to retrieve the `Reactive Data Client` specific part of the store's state tree.

## controller

[Controller](./Controller.md) instance to use.
2 changes: 1 addition & 1 deletion docs/core/api/makeRenderDataClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface ProviderProps {
The Reactive Data Client [&lt;DataProvider /&gt;](./DataProvider.md)

- `import { DataProvider } from @data-client/react;`
- `import { DataProvider } from @data-client/redux;`
- `import { DataProvider } from @data-client/react/redux;`

## renderDataClient()

Expand Down
180 changes: 40 additions & 140 deletions docs/core/guides/redux.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,6 @@ import TabItem from '@theme/TabItem';
Using [redux](https://redux.js.org/) is completely optional. However, for many it means easy integration or migration
with existing projects, or just a nice centralized state management abstraction.

Integration is fairly straightforward as Reactive Data Client already uses the same paradigms as redux under
the hood. However, care should be taken to integrate the reducer and [middlewares](../api/Manager.md) properly
or it won't work as expected.

First make sure you have redux installed:

<PkgTabs pkgs="@data-client/redux redux" />

Note: react-redux is _not_ needed for this integration (though you will need it if you want to use redux directly as well).

Then you'll want to use the [&lt;ExternalDataProvider /\>](../api/ExternalDataProvider.md) instead of
[&lt;DataProvider /\>](../api/DataProvider.md) and pass in the store and a selector function to grab
the Reactive Data Client specific part of the state.

:::info Note

You should only use ONE provider; nested another provider will override the previous.

:::

:::info Note

Reactive Data Client manager middlewares return promises, which is different from how redux middlewares work.
Because of this, if you want to integrate both, you'll need to place all redux middlewares
after the `PromiseifyMiddleware` adapter, and place all Reactive Data Client manager middlewares before.

:::

<Tabs
defaultValue="data-client"
values={[
Expand All @@ -50,43 +22,27 @@ values={[
]}>
<TabItem value="data-client">

#### `index.tsx`

```tsx
```tsx title="index.tsx"
import {
SubscriptionManager,
PollingSubscription,
ExternalDataProvider,
PromiseifyMiddleware,
applyManager,
initialState,
createReducer,
NetworkManager,
Controller,
} from '@data-client/redux';
import { createStore, applyMiddleware } from 'redux';
prepareStore,
type Middleware,
} from '@data-client/react/redux';
import { getDefaultManagers, Controller } from '@data-client/react';
import ReactDOM from 'react-dom';

const networkManager = new NetworkManager();
const subscriptionManager = new SubscriptionManager(PollingSubscription);
const controller = new Controller();
const managers = getDefaultManagers();
// be sure to include your other reducers here
const otherReducers = {};
const extraMiddlewares: Middleware = [];

const store = createStore(
createReducer(controller),
const { store, selector, controller } = prepareStore(
initialState,
applyMiddleware(
...applyManager([networkManager, subscriptionManager], controller),
// place Reactive Data Client built middlewares before PromiseifyMiddleware
PromiseifyMiddleware,
// place redux middlewares after PromiseifyMiddleware
),
managers,
Controller,
otherReducers,
extraMiddlewares,
);
const selector = state => state;

// managers optionally provide initialization subroutine
for (const manager of [networkManager, subscriptionManager]) {
manager.init?.(selector(store.getState()));
}

ReactDOM.render(
<ExternalDataProvider
Expand All @@ -103,44 +59,28 @@ ReactDOM.render(
</TabItem>
<TabItem value="react-redux">

#### `index.tsx`

```tsx
```tsx title="index.tsx"
import {
SubscriptionManager,
PollingSubscription,
ExternalDataProvider,
PromiseifyMiddleware,
applyManager,
initialState,
createReducer,
NetworkManager,
Controller,
} from '@data-client/redux';
import { createStore, applyMiddleware } from 'redux';
prepareStore,
type Middleware,
} from '@data-client/react/redux';
import { getDefaultManagers, Controller } from '@data-client/react';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';

const manager = new NetworkManager();
const subscriptionManager = new SubscriptionManager(PollingSubscription);
const controller = new Controller();
const managers = getDefaultManagers();
// be sure to include your other reducers here
const otherReducers = {};
const extraMiddlewares: Middleware = [];

const store = createStore(
createReducer(controller),
const { store, selector, controller } = prepareStore(
initialState,
applyMiddleware(
...applyManager([networkManager, subscriptionManager], controller),
// place Reactive Data Client built middlewares before PromiseifyMiddleware
PromiseifyMiddleware,
// place redux middlewares after PromiseifyMiddleware
),
managers,
Controller,
otherReducers,
extraMiddlewares,
);
const selector = state => state;

// managers optionally provide initialization subroutine
for (const manager of [networkManager, subscriptionManager]) {
manager.init?.(selector(store.getState()));
}

ReactDOM.render(
<ExternalDataProvider
Expand All @@ -159,61 +99,21 @@ ReactDOM.render(
</TabItem>
</Tabs>

Above we have the simplest case where the entire redux store is used for Reactive Data Client.
However, more commonly you will be integrating with other state. In this case, you
will need to use the `selector` prop of `<ExternalDataProvider/>` to specify
where in the state tree the Reactive Data Client information is.

```typescript
// ...
// highlight-next-line
const selector = state => state.dataClient;

const store = createStore(
// Now we have other reducers
// highlight-start
combineReducers({
dataClient: dataClientReducer,
myOtherState: otherReducer,
}),
// highlight-end
applyMiddleware(
...mapMiddleware(selector)(
...applyManager([networkManager, subscriptionManager], controller),
),
PromiseifyMiddleware,
),
);
// ...
```
Then you'll want to use the [&lt;ExternalDataProvider /\>](../api/ExternalDataProvider.md) instead of
[&lt;DataProvider /\>](../api/DataProvider.md) and pass in the store and a selector function to grab
the Reactive Data Client specific part of the state.

Here we store Reactive Data Client state information in the 'dataClient' part of the tree.
:::info Note

## Redux devtools
You should only use ONE provider; nested another provider will override the previous.

:::

[Redux DevTools](https://github.com/reduxjs/redux-devtools) allows easy inspection of current
state and transitions in the Reactive Data Client store.
:::info Note

Simply wrap the return value of `applyMiddleware()` with `composeWithDevTools()`
Because `Reactive Data Client` [manager middlewares](../api/Manager.md#getmiddleware) return promises,
all redux middlewares are placed after the [Managers](../concepts/managers.md).

```typescript
import { composeWithDevTools } from 'redux-devtools-extension';
If you need a middlware to run before the managers, you will need to wrap it in a [manager](../api/Manager.md).

const store = createStore(
createReducer(controller),
initialState,
// highlight-start
composeWithDevTools({
trace: true,
})(
// highlight-end
applyMiddleware(
...applyManager([networkManager, subscriptionManager], controller),
// place Reactive Data Client built middlewares before PromiseifyMiddleware
PromiseifyMiddleware,
// place redux middlewares after PromiseifyMiddleware
),
// highlight-next-line
),
);
```
:::
2 changes: 1 addition & 1 deletion docs/core/guides/render-as-you-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ or [useSuspense()](../api/useSuspense) actually only dispatch the request to fet
then uses its global awareness to determine whether to fetch. This means, for instance, that
duplicate requests for data can be deduped into one fetch, with one promise to resolve.

Another interesting implication is that fetches started imperatively via [Controller.fetch()](../api/Controller.md#fetch)
Another interesting implication is that fetches started imperatively via [Controller.fetchIfStale()](../api/Controller.md#fetchIfStale) and [Controller.fetch()](../api/Controller.md#fetch)
won't result in redundant fetches. This is known as 'fetch as you render,' and often results
in an improved user experience.

Expand Down
4 changes: 2 additions & 2 deletions docs/core/guides/unit-testing-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ require('whatwg-fetch');
defaultValue="DataProvider"
values={[
{ label: '@data-client/react', value: 'DataProvider' },
{ label: '@data-client/redux', value: 'ExternalDataProvider' },
{ label: '@data-client/react/redux', value: 'ExternalDataProvider' },
]}>
<TabItem value="DataProvider">

Expand Down Expand Up @@ -115,7 +115,7 @@ describe('useSuspense()', () => {
```typescript
import nock from 'nock';
import { makeRenderDataClient } from '@data-client/test';
import { DataProvider } from '@data-client/redux';
import { DataProvider } from '@data-client/react/redux';

describe('useSuspense()', () => {
let renderDataClient: ReturnType<typeof makeRenderDataClient>;
Expand Down
2 changes: 2 additions & 0 deletions docs/core/shared/_installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ ReactDOM.createRoot(document.body).render(
);
```

Alternatively [integrate state with redux](../guides/redux.md)

</TabItem>

<TabItem value="native">
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ const baseConfig = {
'packages/rest/src/next',
'packages/core/src/next',
'packages/react/src/next',
'packages/react/src/server',
'packages/react/src/components/DevToolsButton.tsx',
],
testEnvironmentOptions: {
url: 'http://localhost',
},
/** TODO: Remove once we move to 'publishConfig' */
moduleNameMapper: {
'@data-client/react/redux$': ['<rootDir>/packages/react/src/server/redux'],
'@data-client/([^/]+)(/.*|[^/]*)$': ['<rootDir>/packages/$1/src$2'],
},
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"build:types": "yarn build:copy:ambient && tsc --build && yarn workspaces foreach -Wpti --no-private run build:legacy-types",
"ci:build:types": "yarn build:copy:ambient && tsc --build && yarn workspaces foreach -WptivR --from @data-client/react --from @data-client/rest --from @data-client/graphql --from @data-client/hooks run build:legacy-types",
"ci:build": "yarn workspaces foreach -WptivR --from @data-client/react --from @data-client/rest --from @data-client/graphql --from @data-client/hooks --from @data-client/test run build:lib && yarn workspace @data-client/test run build:bundle && yarn workspace @data-client/normalizr run build:js:node && yarn workspace @data-client/endpoint run build:js:node",
"build:copy:ambient": "mkdirp ./packages/endpoint/lib && copyfiles --flat ./packages/endpoint/src/schema.d.ts ./packages/endpoint/lib/ && copyfiles --flat ./packages/endpoint/src/endpoint.d.ts ./packages/endpoint/lib/ && copyfiles --flat ./packages/rest/src/RestEndpoint.d.ts ./packages/rest/lib && copyfiles --flat ./packages/rest/src/next/RestEndpoint.d.ts ./packages/rest/lib/next",
"build:copy:ambient": "mkdirp ./packages/endpoint/lib && copyfiles --flat ./packages/endpoint/src/schema.d.ts ./packages/endpoint/lib/ && copyfiles --flat ./packages/endpoint/src/endpoint.d.ts ./packages/endpoint/lib/ && mkdirp ./packages/rest/lib && copyfiles --flat ./packages/rest/src/RestEndpoint.d.ts ./packages/rest/lib && copyfiles --flat ./packages/rest/src/next/RestEndpoint.d.ts ./packages/rest/lib/next && mkdirp ./packages/react/lib && copyfiles --flat ./packages/react/src/server/redux/redux.d.ts ./packages/react/lib/server/redux",
"copy:websitetypes": "./scripts/copywebsitetypes.sh",
"test": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest",
"test:ci": "yarn test --ci",
Expand Down
Loading

0 comments on commit 428ddd1

Please sign in to comment.