Skip to content

Commit 4d8a8a5

Browse files
committed
fix: SSR hydration when removing devtools manager
1 parent 6d3c29e commit 4d8a8a5

File tree

12 files changed

+96
-171
lines changed

12 files changed

+96
-171
lines changed

.changeset/hip-shirts-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@data-client/react': patch
3+
---
4+
5+
Fix SSR hydration when removing devtools manager

packages/react/src/components/DataProvider.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ See https://dataclient.io/docs/guides/ssr.`,
7171
managersRef.current,
7272
);
7373

74+
// only include if they have devtools integrated
75+
const hasDevManager = !!managersRef.current.find(
76+
manager => manager instanceof DevToolsManager,
77+
);
7478
return (
7579
<ControllerContext.Provider value={controllerRef.current}>
7680
<DataStore
@@ -81,7 +85,7 @@ See https://dataclient.io/docs/guides/ssr.`,
8185
>
8286
{children}
8387
</DataStore>
84-
{renderDevButton(devButton, managersRef.current)}
88+
{renderDevButton(devButton, hasDevManager)}
8589
</ControllerContext.Provider>
8690
);
8791
}

packages/react/src/components/renderDevButton.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { DevToolsManager, type Manager } from '@data-client/core';
21
import { lazy } from 'react';
32

43
import type { DevToolsPosition } from './DevToolsButton.js';
@@ -7,14 +6,10 @@ import UniversalSuspense from './UniversalSuspense.js';
76

87
export function renderDevButton(
98
devButton: DevToolsPosition | null | undefined,
10-
managers: Manager[],
9+
hasDevManager: boolean,
1110
) {
1211
/* istanbul ignore else */
13-
if (
14-
process.env.NODE_ENV !== 'production' &&
15-
// only include if they have devtools integrated
16-
managers.find(manager => manager instanceof DevToolsManager)
17-
) {
12+
if (process.env.NODE_ENV !== 'production' && hasDevManager) {
1813
return (
1914
<UniversalSuspense fallback={null}>
2015
{

packages/react/src/server/createPersistedStore.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { useSyncExternalStore } from 'react';
1313
import { ExternalDataProvider, PromiseifyMiddleware } from './redux/index.js';
1414
import { createStore, applyMiddleware } from './redux/redux.js';
1515

16-
export default function createPersistedStore(managers?: Manager[]) {
16+
export default function createPersistedStore(
17+
managers?: Manager[],
18+
hasDevManager: boolean = true,
19+
) {
1720
const controller = new Controller();
1821
managers = managers ?? [new NetworkManager()];
1922
const nm: NetworkManager = managers.find(
@@ -56,6 +59,7 @@ export default function createPersistedStore(managers?: Manager[]) {
5659
store={store}
5760
selector={selector}
5861
controller={controller}
62+
hasDevManager={hasDevManager}
5963
>
6064
{children}
6165
</ExternalDataProvider>

packages/react/src/server/nextjs/DataProvider/createPersistedStoreServer.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createReducer,
77
initialState,
88
applyManager,
9+
DevToolsManager,
910
} from '@data-client/core';
1011
import type { ComponentProps } from 'react';
1112

@@ -61,12 +62,22 @@ export default function createPersistedStore(managers?: Manager[]) {
6162
return getState();
6263
})();
6364

64-
const StoreDataProvider = ({ children }: ProviderProps) => {
65+
const StoreDataProvider = ({
66+
children,
67+
devButton,
68+
managers,
69+
}: ProviderProps) => {
70+
// only include if they have devtools integrated
71+
const hasDevManager = !!managers?.find(
72+
manager => manager instanceof DevToolsManager,
73+
);
6574
return (
6675
<ExternalDataProvider
6776
store={store}
6877
selector={selector}
6978
controller={controller}
79+
devButton={devButton}
80+
hasDevManager={hasDevManager}
7081
>
7182
{children}
7283
</ExternalDataProvider>

packages/react/src/server/redux/DataProvider.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
'use client';
2-
import type { Controller, Manager, State } from '@data-client/core';
2+
import {
3+
DevToolsManager,
4+
type Controller,
5+
type Manager,
6+
type State,
7+
} from '@data-client/core';
38
import React, { useEffect, useMemo } from 'react';
49

510
import ExternalCacheProvider from './ExternalDataProvider.js';
611
import { prepareStore } from './prepareStore.js';
12+
import { DevToolsPosition } from '../../components/DevToolsButton.js';
713

814
/** For usage with https://dataclient.io/docs/api/makeRenderDataClient */
915
export default function ExternalDataProvider({
1016
children,
1117
managers,
1218
initialState,
1319
Controller,
20+
devButton = 'bottom-right',
1421
}: Props) {
1522
const { selector, store, controller } = useMemo(
1623
() => prepareStore(initialState, managers, Controller),
@@ -33,11 +40,18 @@ export default function ExternalDataProvider({
3340
// eslint-disable-next-line react-hooks/exhaustive-deps
3441
}, [...managers]);
3542

43+
// only include if they have devtools integrated
44+
const hasDevManager = !!managers.find(
45+
manager => manager instanceof DevToolsManager,
46+
);
47+
3648
return (
3749
<ExternalCacheProvider
3850
store={store}
3951
selector={selector}
4052
controller={controller}
53+
devButton={devButton}
54+
hasDevManager={hasDevManager}
4155
>
4256
{children}
4357
</ExternalCacheProvider>
@@ -49,4 +63,5 @@ interface Props {
4963
managers: Manager[];
5064
initialState: State<unknown>;
5165
Controller: typeof Controller;
66+
devButton?: DevToolsPosition | null | undefined;
5267
}

packages/react/src/server/redux/ExternalDataProvider.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import React, {
1313
useCallback,
1414
} from 'react';
1515

16+
import type { DevToolsPosition } from '../../components/DevToolsButton.js';
17+
import { renderDevButton } from '../../components/renderDevButton.js';
1618
import {
1719
ControllerContext,
1820
StoreContext,
@@ -32,6 +34,8 @@ interface Props<S> {
3234
store: Store<S>;
3335
selector: (state: S) => State<unknown>;
3436
controller: Controller;
37+
devButton?: DevToolsPosition | null | undefined;
38+
hasDevManager?: boolean;
3539
}
3640

3741
/**
@@ -43,6 +47,8 @@ export default function ExternalDataProvider<S>({
4347
store,
4448
selector,
4549
controller,
50+
devButton = 'bottom-right',
51+
hasDevManager = false,
4652
}: Props<S>) {
4753
const masterReducer = useMemo(() => createReducer(controller), [controller]);
4854
const selectState = useCallback(() => {
@@ -80,9 +86,7 @@ export default function ExternalDataProvider<S>({
8086
<UniversalSuspense fallback={<BackupLoading />}>
8187
{children}
8288
</UniversalSuspense>
83-
{process.env.NODE_ENV !== 'production' ?
84-
<UniversalSuspense fallback={null} />
85-
: undefined}
89+
{renderDevButton(devButton, hasDevManager)}
8690
</ControllerContext.Provider>
8791
</StoreContext.Provider>
8892
</StateContext.Provider>

packages/react/src/server/redux/redux.d.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -291,23 +291,23 @@ interface Store<S = any, A extends Action = UnknownAction, StateExt = unknown> {
291291
* @returns A function to remove this change listener.
292292
*/
293293
subscribe(listener: ListenerCallback): Unsubscribe;
294-
/**
295-
* Replaces the reducer currently used by the store to calculate the state.
296-
*
297-
* You might need this if your app implements code splitting and you want to
298-
* load some of the reducers dynamically. You might also need this if you
299-
* implement a hot reloading mechanism for Redux.
300-
*
301-
* @param nextReducer The reducer for the store to use instead.
302-
*/
303-
replaceReducer(nextReducer: Reducer<S, A>): void;
304-
/**
305-
* Interoperability point for observable/reactive libraries.
306-
* @returns {observable} A minimal observable of state changes.
307-
* For more information, see the observable proposal:
308-
* https://github.com/tc39/proposal-observable
309-
*/
310-
[Symbol.observable](): Observable<S & StateExt>;
294+
// /**
295+
// * Replaces the reducer currently used by the store to calculate the state.
296+
// *
297+
// * You might need this if your app implements code splitting and you want to
298+
// * load some of the reducers dynamically. You might also need this if you
299+
// * implement a hot reloading mechanism for Redux.
300+
// *
301+
// * @param nextReducer The reducer for the store to use instead.
302+
// */
303+
// replaceReducer(nextReducer: Reducer<S, A>): void;
304+
// /**
305+
// * Interoperability point for observable/reactive libraries.
306+
// * @returns {observable} A minimal observable of state changes.
307+
// * For more information, see the observable proposal:
308+
// * https://github.com/tc39/proposal-observable
309+
// */
310+
// [Symbol.observable](): Observable<S & StateExt>;
311311
}
312312
type UnknownIfNonSpecific<T> = {} extends T ? unknown : T;
313313
/**

packages/react/src/server/redux/redux.js

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ export function formatProdErrorMessage(code) {
33
return `Minified Redux error #${code}; visit https://redux.js.org/Errors?code=${code} for the full message or use the non-minified dev environment for full errors. `;
44
}
55

6-
// src/utils/symbol-observable.ts
7-
var $$observable = /* @__PURE__ */ (() =>
8-
(typeof Symbol === 'function' && Symbol.observable) || '@@observable')();
9-
var symbol_observable_default = $$observable;
10-
116
// src/utils/actionTypes.ts
127
var randomString = () =>
138
Math.random().toString(36).substring(7).split('').join('.');
@@ -224,64 +219,13 @@ function createStore(reducer, preloadedState, enhancer) {
224219
});
225220
return action;
226221
}
227-
function replaceReducer(nextReducer) {
228-
if (typeof nextReducer !== 'function') {
229-
throw new Error(
230-
process.env.NODE_ENV === 'production' ?
231-
formatProdErrorMessage(10)
232-
: `Expected the nextReducer to be a function. Instead, received: '${kindOf(nextReducer)}`,
233-
);
234-
}
235-
currentReducer = nextReducer;
236-
dispatch({
237-
type: actionTypes_default.REPLACE,
238-
});
239-
}
240-
function observable() {
241-
const outerSubscribe = subscribe;
242-
return {
243-
/**
244-
* The minimal observable subscription method.
245-
* @param observer Any object that can be used as an observer.
246-
* The observer object should have a `next` method.
247-
* @returns An object with an `unsubscribe` method that can
248-
* be used to unsubscribe the observable from the store, and prevent further
249-
* emission of values from the observable.
250-
*/
251-
subscribe(observer) {
252-
if (typeof observer !== 'object' || observer === null) {
253-
throw new Error(
254-
process.env.NODE_ENV === 'production' ?
255-
formatProdErrorMessage(11)
256-
: `Expected the observer to be an object. Instead, received: '${kindOf(observer)}'`,
257-
);
258-
}
259-
function observeState() {
260-
const observerAsObserver = observer;
261-
if (observerAsObserver.next) {
262-
observerAsObserver.next(getState());
263-
}
264-
}
265-
observeState();
266-
const unsubscribe = outerSubscribe(observeState);
267-
return {
268-
unsubscribe,
269-
};
270-
},
271-
[symbol_observable_default]() {
272-
return this;
273-
},
274-
};
275-
}
276222
dispatch({
277223
type: actionTypes_default.INIT,
278224
});
279225
const store = {
280226
dispatch,
281227
subscribe,
282228
getState,
283-
replaceReducer,
284-
[symbol_observable_default]: observable,
285229
};
286230
return store;
287231
}

website/src/components/Playground/editor-types/@data-client/core.d.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,7 @@ declare class Controller<D extends GenericDispatch = DataClientDispatch> {
485485
* Forces refetching and suspense on useSuspense with the same Endpoint and parameters.
486486
* @see https://dataclient.io/docs/api/Controller#invalidate
487487
*/
488-
invalidate: <E extends EndpointInterface<FetchFunction, Schema | undefined, boolean | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [
489-
null
490-
]) => Promise<void>;
488+
invalidate: <E extends EndpointInterface<FetchFunction, Schema | undefined, boolean | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]) => Promise<void>;
491489
/**
492490
* Forces refetching and suspense on useSuspense on all matching endpoint result keys.
493491
* @see https://dataclient.io/docs/api/Controller#invalidateAll
@@ -549,16 +547,12 @@ declare class Controller<D extends GenericDispatch = DataClientDispatch> {
549547
* Marks a new subscription to a given Endpoint.
550548
* @see https://dataclient.io/docs/api/Controller#subscribe
551549
*/
552-
subscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [
553-
null
554-
]) => Promise<void>;
550+
subscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]) => Promise<void>;
555551
/**
556552
* Marks completion of subscription to a given Endpoint.
557553
* @see https://dataclient.io/docs/api/Controller#unsubscribe
558554
*/
559-
unsubscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [
560-
null
561-
]) => Promise<void>;
555+
unsubscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]) => Promise<void>;
562556
/*************** More ***************/
563557
/**
564558
* Gets a snapshot (https://dataclient.io/docs/api/Snapshot)

0 commit comments

Comments
 (0)