Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Fix Client components losing state on refetches #20319

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,14 +442,19 @@ export function resolveModelToJSON(
request.pendingChunks++;
const moduleId = request.nextChunkId++;
emitModuleChunk(request, moduleId, moduleMetaData);
if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
// If we're encoding the "type" of an element, we can refer
// to that by a lazy reference instead of directly since React
// knows how to deal with lazy values. This lets us suspend
// on this component rather than its parent until the code has
// loaded.
return serializeByRefID(moduleId);
}

// Removed because this make lazy "type" different between refetches.
// TODO: add this back?

// if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
// // If we're encoding the "type" of an element, we can refer
// // to that by a lazy reference instead of directly since React
// // knows how to deal with lazy values. This lets us suspend
// // on this component rather than its parent until the code has
// // loaded.
// return serializeByRefID(moduleId);
// }

return serializeByValueID(moduleId);
} catch (x) {
request.pendingChunks++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,92 @@ describe('ReactFlightDOM', () => {
'<p>Game over</p>', // TODO: should not have message in prod.
);
});

// @gate experimental
it('should preserve state of client components on refetch', async () => {
const {Suspense} = React;

// Client

function Page({response}) {
return response.readRoot();
}

function Input() {
return <input />;
}

const InputClient = moduleReference(Input);

// Server

function App({color}) {
// Verify both DOM and Client children.
return (
<div style={{color}}>
<input />
<InputClient />
</div>
);
}

const container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);

const stream1 = getTestStream();
ReactTransportDOMServer.pipeToNodeWritable(
<App color="red" />,
stream1.writable,
webpackMap,
);
const response1 = ReactTransportDOMClient.createFromReadableStream(
stream1.readable,
);
await act(async () => {
root.render(
<Suspense fallback={<p>(loading)</p>}>
<Page response={response1} />
</Suspense>,
);
});
expect(container.children.length).toBe(1);
expect(container.children[0].tagName).toBe('DIV');
expect(container.children[0].style.color).toBe('red');

// Change the DOM state for both inputs.
const inputA = container.children[0].children[0];
expect(inputA.tagName).toBe('INPUT');
inputA.value = 'hello';
const inputB = container.children[0].children[1];
expect(inputB.tagName).toBe('INPUT');
inputB.value = 'goodbye';

const stream2 = getTestStream();
ReactTransportDOMServer.pipeToNodeWritable(
<App color="blue" />,
stream2.writable,
webpackMap,
);
const response2 = ReactTransportDOMClient.createFromReadableStream(
stream2.readable,
);
await act(async () => {
root.render(
<Suspense fallback={<p>(loading)</p>}>
<Page response={response2} />
</Suspense>,
);
});
expect(container.children.length).toBe(1);
expect(container.children[0].tagName).toBe('DIV');
expect(container.children[0].style.color).toBe('blue');

// Verify we didn't destroy the DOM for either input.
expect(inputA === container.children[0].children[0]).toBe(true);
expect(inputA.tagName).toBe('INPUT');
expect(inputA.value).toBe('hello');
expect(inputB === container.children[0].children[1]).toBe(true);
expect(inputB.tagName).toBe('INPUT');
expect(inputB.value).toBe('goodbye');
});
});