2

Edit: Check out the git repository for a minmal example: https://github.com/maximilianschmitt/blind-lifecycle

I have a component RequireUser that tries to ensure that the user is logged in and will otherwise not render its children. Its parent component, App, should know if a user is required and render a login form if needed.

The problem is, that the App component mounts AFTER the RequireUser component in a tree like this:

App
  RequireUser
    SomeOtherComponent

In RequireUser's componentDidMount I am triggering an action requireLogin that sets the UserStore's loginRequired variable to true.

This does not update the parent component (App) because it has not yet been mounted and can therefor not register changes to the store.

class RequireUser extends React.Component {
  constructor() {
    super();
    this.state = alt.stores.UserStore.getState();
  }

  componentDidMount() {
    this.unlisten = alt.stores.UserStore.listen(this.setState.bind(this));
    if (!this.state.requireUser) {
      UserActions.requireUser();
      // using setTimeout will work:
      // setTimeout(() => UserActions.requireUser());
    }
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    if (this.state.requireUser) {
      return <div>I have required your user</div>;
    }

    return <div>I will require your user</div>;
  }
}

class App extends React.Component {
  constructor() {
    super();
    this.state = alt.stores.UserStore.getState();
  }

  componentDidMount() {
    this.unlisten = alt.stores.UserStore.listen(this.setState.bind(this));
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    return (
      <div>
        <div>User required? {this.state.requireUser + ''}</div>
        <RequireUser />
      </div>
    );
  }
}

Output:

User required? false

I have required your user

If I use setTimeout in RequireUser, App receives the state changes and renders, but only after a flicker:

User required? true

I have required your user

I have the feeling what I am doing is an anti-pattern and I would be grateful for suggestions of a more elegant solution than flickering with setTimeout. Thanks!

Community
  • 1
  • 1
wundermax
  • 23
  • 3
  • Are you having further issues moving on? If your problem is solved then you should mark the answer that helped resolving the problem as **accepted**. This helps other people to find what they are looking for. Happy Coding :) – Ling Zhong Nov 08 '15 at 23:15
  • Thanks Ling, I'm still seeing if someone has a solution that will not require me rendering twice. I will mark the correct answer as accepted. – wundermax Nov 09 '15 at 09:48

3 Answers3

1

My suggested answer is to add this to the App component:

componentDidMount() {
    // setup listener for subsequent changes
    alt.stores.UserStore.listen(this.onChange);

    // grab the current state now that we're mounted
    var userStoreState = alt.stores.UserStore.getState();
    this.setState(userStoreState);
}

There is no way to avoid the double render. Your RequireUser component already performs two renders.

  1. Initial render of RequireUser
  2. componentDidMount() callback
    • an action is dispatched
  3. UserStore receives the dispatched action and updates its state
    • change notification is emitted
  4. RequireUser sets state based on the state change
  5. Second render of RequireUser

But your codebase is still considered Flux, and indeed follows the pattern intended for React apps. Essentially, you have a loading state... a state where we don't actually know if we need to require a user or not. Depending on what UserActions.requireUser() does, this may or may not be desired.

You might consider a refactor

You can fix the double-render if you rewrite RequireUser as a view-only component. This means no listeners nor setting state internally. This component simply renders elements based on the props passed in. This is literally all your RequireUser component would be:

class RequireUser extends React.Component {
    render() {
        if (this.props.requireUser) {
            return <div>I have required your user</div>;
        }
        return <div>I will require your user</div>;
     }
}

You will then make your App component a controller-view. The listener is added here, and any changes to state are propagated downward by props. Now we can setup in the componentWillMount callback. This gives us the single render behavior.

class App extends React.Component {
    (other lifecycle methods)

    componentWillMount() {
        if (!this.state.requireUser) {
            UserActions.requireUser();
        }
        var userStoreState = alt.stores.UserStore.getState();
        this.setState(userStoreState);
    }

    componentDidMount() {
        (same as above)
    }

    render() {
        return (
            <div>
                <div>User required? {this.state.requireUser + ''}</div>
                <RequireUser requireUser={this.state.requireUser} />
            </div>
       );
    }
}

Flux architecture and controller-views/views: https://facebook.github.io/flux/docs/overview.html#views-and-controller-views

Toby Liu
  • 1,267
  • 9
  • 14
  • Thanks Toby, I already tried this and decided not to go with it, because eslint suggests not to use `setState` in `componentDidMount`. It triggers an additional render so it's not so great unfortunately, even though it does work. Any other ideas? – wundermax Nov 06 '15 at 12:33
  • Looks like componentWillMount() might actually do the trick. According to docs, if you do a setState() during this callback, render will only be executed once with those updates. https://facebook.github.io/react/docs/component-specs.html#mounting-componentwillmount What do you think? – Toby Liu Nov 06 '15 at 16:43
  • Hey Toby! Unfortunately, `componentWillMount` of the parent component is called before `componentDidMount` of the child component. Setting up a listener here would probably work, but I'm not very keen on this, as it will cause a memory leak with server-side rendering. – wundermax Nov 09 '15 at 09:47
  • Alright, you since you are still on this, I will try to help more! Basically, you cannot avoid that double render. However, you might look closer into controller-views as part of Flux architecture. I've got an updated answer for you now... – Toby Liu Nov 10 '15 at 05:08
1

Your components each only gets the states from your Store once - only during the construction of each components. This means that the states in your components will NOT be in sync with the states in the store

You need to set up a store listeners on your components upon mounting in order to retrieve a trigger from the store and the most up-to-date states. Use setState() to update the states inside the component so render() will be called again to render the up-to-date states

Ling Zhong
  • 1,744
  • 14
  • 24
-1

What about putting the store listener in the constructor? That worked for me.

Hahn
  • 1
  • 2