Skip to content

Commit

Permalink
fix: SSR hydration when removing devtools manager
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Jun 19, 2024
1 parent 6d3c29e commit d9ac06a
Show file tree
Hide file tree
Showing 12 changed files with 97 additions and 170 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-shirts-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@data-client/react': patch
---

Fix SSR hydration when removing devtools manager
6 changes: 5 additions & 1 deletion packages/react/src/components/DataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ See https://dataclient.io/docs/guides/ssr.`,
managersRef.current,
);

// only include if they have devtools integrated
const hasDevManager = !!managersRef.current.find(
manager => manager instanceof DevToolsManager,
);
return (
<ControllerContext.Provider value={controllerRef.current}>
<DataStore
Expand All @@ -81,7 +85,7 @@ See https://dataclient.io/docs/guides/ssr.`,
>
{children}
</DataStore>
{renderDevButton(devButton, managersRef.current)}
{renderDevButton(devButton, hasDevManager)}
</ControllerContext.Provider>
);
}
Expand Down
9 changes: 2 additions & 7 deletions packages/react/src/components/renderDevButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DevToolsManager, type Manager } from '@data-client/core';
import { lazy } from 'react';

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

export function renderDevButton(
devButton: DevToolsPosition | null | undefined,
managers: Manager[],
hasDevManager: boolean,
) {
/* istanbul ignore else */
if (
process.env.NODE_ENV !== 'production' &&
// only include if they have devtools integrated
managers.find(manager => manager instanceof DevToolsManager)
) {
if (process.env.NODE_ENV !== 'production' && hasDevManager) {
return (
<UniversalSuspense fallback={null}>
{
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/server/createPersistedStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
initialState,
createReducer,
applyManager,
DevToolsManager,
} from '@data-client/core';
import { useSyncExternalStore } from 'react';

Expand Down Expand Up @@ -50,12 +51,17 @@ export default function createPersistedStore(managers?: Manager[]) {
return useSyncExternalStore(store.subscribe, getState, getState);
}

// only include if they have devtools integrated
const hasDevManager = !!managers?.find(
manager => manager instanceof DevToolsManager,
);
function ServerDataProvider({ children }: { children: React.ReactNode }) {
return (
<ExternalDataProvider
store={store}
selector={selector}
controller={controller}
hasDevManager={hasDevManager}
>
{children}
</ExternalDataProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createReducer,
initialState,
applyManager,
DevToolsManager,
} from '@data-client/core';
import type { ComponentProps } from 'react';

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

const StoreDataProvider = ({ children }: ProviderProps) => {
const StoreDataProvider = ({
children,
devButton,
managers,
}: ProviderProps) => {
// only include if they have devtools integrated
const hasDevManager = !!managers?.find(
manager => manager instanceof DevToolsManager,
);
return (
<ExternalDataProvider
store={store}
selector={selector}
controller={controller}
devButton={devButton}
hasDevManager={hasDevManager}
>
{children}
</ExternalDataProvider>
Expand Down
17 changes: 16 additions & 1 deletion packages/react/src/server/redux/DataProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
'use client';
import type { Controller, Manager, State } from '@data-client/core';
import {
DevToolsManager,
type Controller,
type Manager,
type State,
} from '@data-client/core';
import React, { useEffect, useMemo } from 'react';

import ExternalCacheProvider from './ExternalDataProvider.js';
import { prepareStore } from './prepareStore.js';
import { DevToolsPosition } from '../../components/DevToolsButton.js';

/** For usage with https://dataclient.io/docs/api/makeRenderDataClient */
export default function ExternalDataProvider({
children,
managers,
initialState,
Controller,
devButton = 'bottom-right',
}: Props) {
const { selector, store, controller } = useMemo(
() => prepareStore(initialState, managers, Controller),
Expand All @@ -33,11 +40,18 @@ export default function ExternalDataProvider({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...managers]);

// only include if they have devtools integrated
const hasDevManager = !!managers.find(
manager => manager instanceof DevToolsManager,
);

return (
<ExternalCacheProvider
store={store}
selector={selector}
controller={controller}
devButton={devButton}
hasDevManager={hasDevManager}
>
{children}
</ExternalCacheProvider>
Expand All @@ -49,4 +63,5 @@ interface Props {
managers: Manager[];
initialState: State<unknown>;
Controller: typeof Controller;
devButton?: DevToolsPosition | null | undefined;
}
10 changes: 7 additions & 3 deletions packages/react/src/server/redux/ExternalDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import React, {
useCallback,
} from 'react';

import type { DevToolsPosition } from '../../components/DevToolsButton.js';
import { renderDevButton } from '../../components/renderDevButton.js';
import {
ControllerContext,
StoreContext,
Expand All @@ -32,6 +34,8 @@ interface Props<S> {
store: Store<S>;
selector: (state: S) => State<unknown>;
controller: Controller;
devButton?: DevToolsPosition | null | undefined;
hasDevManager?: boolean;
}

/**
Expand All @@ -43,6 +47,8 @@ export default function ExternalDataProvider<S>({
store,
selector,
controller,
devButton = 'bottom-right',
hasDevManager = false,
}: Props<S>) {
const masterReducer = useMemo(() => createReducer(controller), [controller]);
const selectState = useCallback(() => {
Expand Down Expand Up @@ -80,9 +86,7 @@ export default function ExternalDataProvider<S>({
<UniversalSuspense fallback={<BackupLoading />}>
{children}
</UniversalSuspense>
{process.env.NODE_ENV !== 'production' ?
<UniversalSuspense fallback={null} />
: undefined}
{renderDevButton(devButton, hasDevManager)}
</ControllerContext.Provider>
</StoreContext.Provider>
</StateContext.Provider>
Expand Down
34 changes: 17 additions & 17 deletions packages/react/src/server/redux/redux.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,23 +291,23 @@ interface Store<S = any, A extends Action = UnknownAction, StateExt = unknown> {
* @returns A function to remove this change listener.
*/
subscribe(listener: ListenerCallback): Unsubscribe;
/**
* Replaces the reducer currently used by the store to calculate the state.
*
* You might need this if your app implements code splitting and you want to
* load some of the reducers dynamically. You might also need this if you
* implement a hot reloading mechanism for Redux.
*
* @param nextReducer The reducer for the store to use instead.
*/
replaceReducer(nextReducer: Reducer<S, A>): void;
/**
* Interoperability point for observable/reactive libraries.
* @returns {observable} A minimal observable of state changes.
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
[Symbol.observable](): Observable<S & StateExt>;
// /**
// * Replaces the reducer currently used by the store to calculate the state.
// *
// * You might need this if your app implements code splitting and you want to
// * load some of the reducers dynamically. You might also need this if you
// * implement a hot reloading mechanism for Redux.
// *
// * @param nextReducer The reducer for the store to use instead.
// */
// replaceReducer(nextReducer: Reducer<S, A>): void;
// /**
// * Interoperability point for observable/reactive libraries.
// * @returns {observable} A minimal observable of state changes.
// * For more information, see the observable proposal:
// * https://github.com/tc39/proposal-observable
// */
// [Symbol.observable](): Observable<S & StateExt>;
}
type UnknownIfNonSpecific<T> = {} extends T ? unknown : T;
/**
Expand Down
56 changes: 0 additions & 56 deletions packages/react/src/server/redux/redux.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ export function formatProdErrorMessage(code) {
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. `;
}

// src/utils/symbol-observable.ts
var $$observable = /* @__PURE__ */ (() =>
(typeof Symbol === 'function' && Symbol.observable) || '@@observable')();
var symbol_observable_default = $$observable;

// src/utils/actionTypes.ts
var randomString = () =>
Math.random().toString(36).substring(7).split('').join('.');
Expand Down Expand Up @@ -224,64 +219,13 @@ function createStore(reducer, preloadedState, enhancer) {
});
return action;
}
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error(
process.env.NODE_ENV === 'production' ?
formatProdErrorMessage(10)
: `Expected the nextReducer to be a function. Instead, received: '${kindOf(nextReducer)}`,
);
}
currentReducer = nextReducer;
dispatch({
type: actionTypes_default.REPLACE,
});
}
function observable() {
const outerSubscribe = subscribe;
return {
/**
* The minimal observable subscription method.
* @param observer Any object that can be used as an observer.
* The observer object should have a `next` method.
* @returns An object with an `unsubscribe` method that can
* be used to unsubscribe the observable from the store, and prevent further
* emission of values from the observable.
*/
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new Error(
process.env.NODE_ENV === 'production' ?
formatProdErrorMessage(11)
: `Expected the observer to be an object. Instead, received: '${kindOf(observer)}'`,
);
}
function observeState() {
const observerAsObserver = observer;
if (observerAsObserver.next) {
observerAsObserver.next(getState());
}
}
observeState();
const unsubscribe = outerSubscribe(observeState);
return {
unsubscribe,
};
},
[symbol_observable_default]() {
return this;
},
};
}
dispatch({
type: actionTypes_default.INIT,
});
const store = {
dispatch,
subscribe,
getState,
replaceReducer,
[symbol_observable_default]: observable,
};
return store;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,7 @@ declare class Controller<D extends GenericDispatch = DataClientDispatch> {
* Forces refetching and suspense on useSuspense with the same Endpoint and parameters.
* @see https://dataclient.io/docs/api/Controller#invalidate
*/
invalidate: <E extends EndpointInterface<FetchFunction, Schema | undefined, boolean | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [
null
]) => Promise<void>;
invalidate: <E extends EndpointInterface<FetchFunction, Schema | undefined, boolean | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]) => Promise<void>;
/**
* Forces refetching and suspense on useSuspense on all matching endpoint result keys.
* @see https://dataclient.io/docs/api/Controller#invalidateAll
Expand Down Expand Up @@ -549,16 +547,12 @@ declare class Controller<D extends GenericDispatch = DataClientDispatch> {
* Marks a new subscription to a given Endpoint.
* @see https://dataclient.io/docs/api/Controller#subscribe
*/
subscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [
null
]) => Promise<void>;
subscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]) => Promise<void>;
/**
* Marks completion of subscription to a given Endpoint.
* @see https://dataclient.io/docs/api/Controller#unsubscribe
*/
unsubscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [
null
]) => Promise<void>;
unsubscribe: <E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]) => Promise<void>;
/*************** More ***************/
/**
* Gets a snapshot (https://dataclient.io/docs/api/Snapshot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,16 @@ type ReducersMapObject<S = any, A extends {
[K in keyof S]: Reducer<S[K], A, K extends keyof PreloadedState ? PreloadedState[K] : never>;
} : never;

type DevToolsPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';

/** For usage with https://dataclient.io/docs/api/makeRenderDataClient */
declare function ExternalDataProvider$1({ children, managers, initialState, Controller, }: Props$1): react_jsx_runtime.JSX.Element;
declare function ExternalDataProvider$1({ children, managers, initialState, Controller, devButton, }: Props$1): react_jsx_runtime.JSX.Element;
interface Props$1 {
children: React$1.ReactNode;
managers: Manager[];
initialState: State<unknown>;
Controller: typeof Controller;
devButton?: DevToolsPosition | null | undefined;
}

interface Store<S> {
Expand All @@ -168,12 +171,14 @@ interface Props<S> {
store: Store<S>;
selector: (state: S) => State<unknown>;
controller: Controller;
devButton?: DevToolsPosition | null | undefined;
hasDevManager?: boolean;
}
/**
* Like DataProvider, but for an external store
* @see https://dataclient.io/docs/api/ExternalDataProvider
*/
declare function ExternalDataProvider<S>({ children, store, selector, controller, }: Props<S>): react_jsx_runtime.JSX.Element;
declare function ExternalDataProvider<S>({ children, store, selector, controller, devButton, hasDevManager, }: Props<S>): react_jsx_runtime.JSX.Element;

declare const mapMiddleware: <M extends Middleware<{}, any, Dispatch<UnknownAction>>[]>(selector: (state: any) => State<unknown>) => (...middlewares: Middleware[]) => M;
//# sourceMappingURL=mapMiddleware.d.ts.map
Expand Down
Loading

0 comments on commit d9ac06a

Please sign in to comment.