3
votes

I have a MaterialUI dialog that has a few text fields, drop downs, and other things on it. Some of these elements need to be set to some value every time the dialog opens or re-opens. Others elements cannot be loaded until certain conditions exist (for example, user data is loaded).

For the 'resetting', I'm using the onEnter function. But the onEnter function doesn't run until entering (duh!)... but the render function, itself, still does - meaning any logic or accessing javascript variables in the JSX will still occur. This leaves the 'onEnter' function ill-equipped to be the place I set up and initialize my dialog.

I also can't use the constructor for setting/resetting this initial state, as the data I need to construct the state might not be available at the time the constructor loads (upon app starting up). Now, I could super-complicate my JSX in my render function and make conditionals for every data point... but that's a lot of overhead for something that gets re-rendered every time the app changes anything. (the material UI dialogs appear run the entire render function even when the 'open' parameter is set to false).

What is the best way to deal with initializing values for a material ui dialog?

Here is a super-dumbed-down example (in real life, imagine getInitialState is a much more complex, slow, and potentially async/network, function) - let's pretend that the user object is not available at app inception and is actually some data pulled or entered long after the app has started. This code fails because "user" is undefined on the first render (which occurs BEFORE the onEnter runs).

constructor(props) {
    super(props);
}

getInitialState = () => {
    return {
        user: {username: "John Doe"}
    }
}

onEnter = () => {
    this.setState(this.getInitialState())
}


render() {
    const { dialogVisibility } = this.props;

    return (
        <Dialog  open={dialogVisibility}  onEnter={this.onEnter}>
            <DialogTitle>
                Hi, {this.state.user.username}
            </DialogTitle>
        </Dialog> );
}

My first instinct was to put in an "isInitialized" variable in state and only let the render return the Dialog if "isInitialized" is true, like so:

constructor(props) {
    super(props);
    this.state = {
        isInitialized: false
    };
}

getInitialState = () => {
    return {
        user: {username: "John Doe"}
    }
}

onEnter = () => {
    this.setState(this.getInitialState(), 
        () => this.setState({isInitialized:true})
    );
}


render() {
    const { dialogVisibility } = this.props;

    if(!this.state.isInitialized) {
        return null;
    }

    return (
        <Dialog  open={dialogVisibility}  onEnter={this.onEnter}>
            <DialogTitle>
                Hi, {this.state.user.username}
            </DialogTitle>
        </Dialog> );
}

As I'm sure you are aware... this didn't work, as we never return the Dialog in order to fire the onEnter event that, in turn, fires the onEnter function and actually initializes the data. I tried changing the !this.state.inInitialized conditional to this:

    if(!this.state.isInitialized) {
        this.onEnter();
        return null;
    }

and that works... but it's gives me a run-time warning: Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.

That brought me to a lot of reading, specifically, this question: Calling setState in render is not avoidable which has really driven home that I shouldn't be just ignoring this warning. Further, this method results in all the logic contained in the return JSX to still occur... even when the dialog isn't "open". Add a bunch of complex dialogs and it kills performance.

Surely there is a 'correct' way to do this. Help? Thoughts?

3

3 Answers

3
votes

What you need conceptually is that when you are freshly opening the dialog, you want to reset some items. So you want to be able to listen for when the value of open changes from false to true.

For hooks, the react guide provides an example for keeping the "old" value of a given item with a usePrevious hook. It is then simply a matter of using useEffect.

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function MyDialog({ dialogVisibility }) {
  const prevVisibility = usePrevious(dialogVisibility);

  useEffect(() => {
    // If it is now open, but was previously not open
    if (dialogVisibility && !prevVisibility) {
      // Reset items here
    }
  }, [dialogVisibility, prevVisibility]);

  return <Dialog open={dialogVisibility}></Dialog>;
}

The same thing can be achieved with classes if you use componentDidUpdate and the previousProps parameter it receives.

export class MyDialog extends Component {
  public componentDidUpdate({ dialogVisibility : prevVisibility }) {
    const { dialogVisibility } = this.props;

    if (dialogVisibility && !prevVisibility) {
      // Reset state here
    }
  }

  public render() {
    const { dialogVisibility } = this.props;

    return <Dialog open={dialogVisibility}></Dialog>;
  }
}
1
votes

You should use componentDidUpdate()

  • This method is not called for the initial render
  • Use this as an opportunity to operate on the DOM when the component has been updated

If you need data preloaded before the dialog is opened, you can use componentDidMount():

  • is invoked immediately after a component is mounted (inserted into the tree)
  • if you need to load data from a remote endpoint, this is a good place to instantiate the network request

React guys added the useEffect hook exactly for cases like the one you are describing, but you would need to refactor to a functional component. Source: https://reactjs.org/docs/hooks-effect.html

0
votes

This can be solved by doing leaving the constructor, getInitialState, and onEnter functions as written and making the following addition of a ternary conditional in the render function :

render() {
    const { dialogVisibility } = this.props;

return (
    <Dialog  open={dialogVisibility}  onEnter={this.onEnter}>
        {this.state.isInitialized && dialogVisibility ? 
        <DialogTitle>
            Hi, {this.state.user.username}
        </DialogTitle> : 'Dialog Not Initialized'}
    </Dialog> );
)}

It actually allows the dialog to use it's "onEnter" appropriately, get the right transitions, and avoid running any extended complex logic in the JSX when rendering while not visible. It also doesn't require a refactor or added programming complexity.

...But, I admit, it feels super 'wrong'.