facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
224.77k stars 45.85k forks source link

Feature Request: Soft Component #17386

Open hackwaly opened 4 years ago

hackwaly commented 4 years ago
import React, {useState} from 'react';
import ReactDOM from 'react-dom';

function PageLayout({title, children}) {
  return <div>
    <h1>{title}</h1>
    <input type="text"/>
    {children}
  </div>;
}
function Page2({setPage}) {
  return <PageLayout title="Page2">
    <button onClick={() => {setPage(() => Page1);}}>Test</button>
  </PageLayout>
}
function Page1({setPage}) {
  return <PageLayout title="Page1">
    <button onClick={() => {setPage(() => Page2);}}>Test</button>
  </PageLayout>
}
function App() {
  let [Page, setPage] = useState(() => Page1);
  return <Page setPage={setPage}/>; 
}
ReactDOM.render(<App />, document.getElementById('app'));

https://codesandbox.io/embed/serene-browser-tehj4?fontsize=14

The above code is most intuitive pattern for build multiple page web app. -- Don't mind the setPage. Just focus Page component returns PageLayout instance.

But react's diff algorithm is not optimized for that pattern. If you click "Test" button. The text you inputed in input will lost.

So I proposal "Soft Component" concept. Two soft component will be treated as same component in diff algorithm. In the example, we change Page1 and Page2 to soft components. Thus solve the problem I shown above.

amazzalel-habib commented 4 years ago

If the type of the component is changed, the diff algo will render the new component, ( if Page1 is rendered, on click react will unmout Page1 and render Page2 hence the PageLayout will also be unmounted ). I think it's just the way you render layout that should be changed.

Because React relies on heuristics, if the assumptions behind them are not met, performance will suffer.

The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven’t found this to be an issue.

Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components. https://reactjs.org/docs/reconciliation.html#tradeoffs

hackwaly commented 4 years ago

I think the Soft Component/Component is like macro/function of lisp. Both useful. Without introduce Soft Component, we must write it as function call instead of component instance form.

miraage commented 4 years ago

The way you compose components is not quite a React-way. As @amazzalel-habib suggested, you might want to restructure components.

If you want to stick to your way (which I really not recommend to), your input state becomes a global state which should be managed accordingly.

MuYunyun commented 4 years ago

The Page1's PageLayout and the Page2's PageLayout are two different components, toggling one to the other means it will complete the full process of life cycle including unMount. In my opinion, the behave is normal.

denis-sokolov commented 3 years ago

The code sample in the issue describes the business case very clearly, it is a very natural way to describe what routes consist of. I would love it if we could make it work.

I would like to argue in favor of a stronger suggestion: to make the above work without any input from the authors. Consider that the code example is an easy mistake to make: the code above will work for a long time re-creating the entire page DOM elements during navigation, and it will only be noticed when some state gets reset unexpectedly (like an input in this case, an image loading, or other). The suggestion might not be practical, but we need to keep in mind this possible mistake.

The detracting commenters above all seem to be focused on describing how the reconciliation algorithm works today, but this gives us no information on how it should work tomorrow (this is similar to a philosophical is–ought problem).

Given the description of the trade-offs take in designing the implementation details of the reconciliation algorithm, I am sure React authors have thought this through. But at the end of the day, this is an unnatural quirk React users either need to remember, or hope to not be bitten by.

KutnerUri commented 2 years ago

this is a very important feature - maybe we should rephrase it as:

escape hatch for subtree destruction, during reconciliation

The existing implementation means that changing the container will destroy the children's state. For example:

function App({someUserChoice}) {
  const Container = someUserChoice ? BlueContainer : RedContainer;

  return <Container>
    <Counter/>
  </Container>
}

function Counter() {
  const [counter, setCounter] = useState(0);
  useInterval(() => setCounter(x=> x+1), 1000);

  return <div>{counter}</div>
}

function BlueContainer({ children }: { children: ReactNode }) {
    return <div style={{ background: "blue", padding: 5 }}>{children}</div>;
}

function RedContainer({ children }: { children: ReactNode }) {
    return <div style={{ background: "red", padding: 5 }}>{children}</div>;
}

In this example, changing the containers, would lose the all react state, re-trigger animations, reset scroll, etc.

https://user-images.githubusercontent.com/5400361/159769932-c4995789-50e7-4b59-b372-c56282ccac41.mp4

Of course we could create a unified container, but it's not always possible. Containers may be large and complicated, and sometimes simply blackboxed.

We need an escape hatch, like this:

function App({someUserChoice}) {
  const Container = someUserChoice ? BlueContainer : RedContainer;

  return <Container
    // DRAFT API! ⚠️
    UNSAFE_superKey="app-conatiner" 
    // option 2:
    UNSAFE_container_with_stable_dom
  >
    <Counter/>
  </Container>
}

This will still have some caveats - the containers must have the same dom structure (changing dom elements could have side effects, like reloading iframes).

The containers should still unmount and mount like a regular component - we wouldn't want their hooks/state to clash.