palantir / blueprint

A React-based UI toolkit for the web
https://blueprintjs.com/
Apache License 2.0
20.71k stars 2.17k forks source link

PanelStack does not re-render on props change #3173

Closed alinzk closed 4 years ago

alinzk commented 5 years ago

Environment

Steps to reproduce

  1. Visit https://codesandbox.io/s/8y15oovmj
  2. Click on the button

Actual behavior

The count on top is updated but not inside the PanelStack

Expected behavior

The count should be updated at both places.

Possible solution

Not sure, but I think passing in the props directly to PanelView here will cause it to re-render

giladgray commented 5 years ago

this is a known limitation of PanelStack. definitely open to reviewing a fix PR @anawaz42. your possible solution seems workable.

invliD commented 5 years ago

This is indeed a known limitation. Your idea, however, will not fix it. The initialPanel is put on the React state of the PanelStack only once, any changes to the prop are ignored after that. The fact that the PanelView doesn't re-render is not relevant here.

There is currently no way to ever change the props of an open panel. We internally use redux-connected panels to make them react to changes in the app. The only possible solution I see is to allow making the PanelStack a controlled component, so you can maintain the entire stack of panels as you wish, including changes to existing panels' props.

alinzk commented 5 years ago

Thanks for responding to this. The explanation does make sense and using connected panels is definitely a good option.

Apart from making component controlled, I think I have some other ideas, I'll work a bit and share them.

cecton commented 4 years ago

I found a very good solution!

Use a React <Context> instead.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

https://reactjs.org/docs/context.html

This is my wrapper component:

const Context = React.createContext({} as MenubarProps);

function Menubar(props: MenubarProps) {
  const [panels, setPanels] = useState<IPanel<{}>[]>([
    {
      component: Settings,
      props: {},
      title: "Settings",
    },
  ]);

  return (
    <Context.Provider value={props}>
      <PanelStack
        className="Menubar"
        initialPanel={panels[0]}
        onOpen={(new_) => setPanels([new_, ...panels])}
        onClose={() => setPanels(panels.slice(1))}
      />
    </Context.Provider>
  );
}

A helper to automatically wrap the panel components:

const withContext = (Component: (props: PanelProps) => any) => (panelProps: any) => (
  <Context.Consumer>{(props) => <Component {...props} {...panelProps} />}</Context.Consumer>
);

Example of one of the panels:

const SelectNetwork = withContext(({
    closePanel, // <--- from PanelStack's props
    setNetwork, ap, setAp, // <--- my custom props
   }: PanelProps) => {

  return (
    <CaptivePortal
      onConnected={(essid) => {
        closePanel();
        setNetwork(essid);
      }}
      onAP={() => {
        closePanel();
        setNetwork("");
      }}
      ap={ap}
      setAp={setAp}
      vertical
    />
  );
});

Type:

type PanelProps = IPanelProps & MenubarProps; // PanelStack's props + my custom props
adidahiya commented 4 years ago

Closing this old issue as it appears it can be worked around with controlled mode (#3601) and there are other approaches to data management (redux, react context, etc.) which allow you to connect indirectly to component trees.