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

[core] List the requirement of each hooks #2319

Merged
merged 4 commits into from
Aug 30, 2021

Conversation

flaviendelangle
Copy link
Member

@flaviendelangle flaviendelangle commented Aug 10, 2021

Introduction

The goal of this PR is to give a clearer view of which hooks is using state / methods / events from other hooks to anticipate the effects of synchronous state updates.

In the example below, I will always use the following hooks in the same order :

const useDataGridComponent = (apiRef, props) => {
  const useFeatureA(apiRef, props);
  const useFeatureB(apiRef, props);
  const useFeatureC(apiRef, props);
}

Current behavior

The hook order matter only for usages that have the same initialization / update timing.

Methods

// `useGridApiMethod` inits synchronously the methods 
// so for a hook upper in the chain it will be accessible in async calls but not in synchronous calls

const useFeatureA = (apiRef, props) => {
  React.useEffect(() => {
    apiRef.current.getB() // will be defined / updated before this call
  })
  
  useGridApiOptionHandler(apiRef, GridEvents.rowClick, () => {
    apiRef.current.getB() // will be defined / updated before this call
  });

  useGridApiOptionHandler(apiRef, GridEvents.rowsSet, () => {
    apiRef.current.getB() // will be defined / updated before this call
  });

  apiRef.current.getB() // will not defined / updated before this call
}

const useFeatureB = (apiRef, props) => {
  useGridApiMethod(apiRef, { getB: console.log }, 'FeatureB')
}

State

// The state init / update is in a `useEffect`
// so for a hook upper in the chain it will not be accessible in `useEffect` calls and for event it's depending a lot on when the events are fired

const featureBSelector = createSelector(state => state.featureB)

const useFeatureA = (apiRef, props) => {
  const featureB = useGridSelector(apiRef, featureBSelector)

  React.useEffect(() => {
    console.log(apiRef.current.state.featureB) // will have the default value on the 1st run and will always be outdated
    console.log(featureB)// will have the default value on the 1st run and will always be outdated
  })
  
  useGridApiOptionHandler(apiRef, GridEvents.rowClick, () => {
    console.log(apiRef.current.state.featureB) // will probably be defined / updated before this call
    console.log(featureB)// will have the default value on the 1st run and will always be outdated
  });

  useGridApiOptionHandler(apiRef, GridEvents.rowsSet, () => {
    console.log(apiRef.current.state.featureB) // will have the default value on the 1st run and will always be outdated
    console.log(featureB)// will have the default value on the 1st run and will always be outdated
  });

  console.log(featureB) // will have the default value on the 1st run and will always be outdated
}

const useFeatureB = (apiRef, props) => {
  React.useEffect(() => {
    setGridState((state) => ({
        ...state,
        featureB: props.featureB,
      }));
    forceUpdate();
  }, [setGridState, forceUpdate, props.featureB, apiRef]);
}

Impact of the state management rework

One of the goal of #2231 is to reduce the setGridState cascade in useEffect that causes chain of re-renders
We have to make some tests to determine the best approach, but the goal is that a change in the props or the call of a DOM event updates synchronously as much of the state as possible.

We have two main approaches right now : apply the derived state in the render (like explained here) or apply the derived state with synchronous events (like we are already doing a lot).
In either way, this should have pretty much the same impact on this PR goal.

The following scenarios should be impacted

Use a state in a now synchronous event

For events, we may bypass the issue by registering the calls and running them all after all hooks are executed.
But it would add some complexity.

Without it, the following example would crash on the 1st render (getB would not be defined) and would show outdated propForFeatureB on future renders.

const useFeatureA = (apiRef, props) => {
  useGridApiOptionHandler(apiRef, GridEvents.rowsSet, () => {
    apiRef.current.getB();
  });
} 

const useFeatureB = (apiRef, props) => {
  const getB = React.useCallback(() => {
    console.log(props.propForFeatureB);
  })

  useGridApiMethod(apiRef, { getB }, 'FeatureB')
}

Use a selector

If we go with the approach n°1 in #2293 , then the state would not be initialized in the 1st run of useFeatureA
If we go with the approach n°2, it would be initialized but for future run it would be outdated because useFeatureB would update it after useFeatureA runs.

const featureBSelector = createSelector(state => state.featureB)

const useFeatureA = (apiRef, props) => {
  const featureB = useGridSelector(apiRef, featureBSelector)
} 

const useFeatureB = (apiRef, props) => {
  useGridSetStateOnUpdate(apiRef, state => ({ 
    ...state,
    featureB: getDerivedFeatureBStateFromProp(
      props.featureB,
      props.otherFeatureBThing 
    )
  }), [props.featureB, props.otherFeatureBThing])
}

// Very incomplete implementation, it's only here to display the behavior of a synchronous state update
const useGridSetStateOnUpdate = (apiRef, updater, deps) => {
  const [, setGridState] = useSetGridState(apiRef)
  const prevDeps = React.useRef()

  if (shallowCompare(prevDeps, deps)) {
    setGridState(updater)
  }
}

Solutions

  • Try to avoid circular feature dependencies (useFeatureA needs state from useFeatureB which needs method from useFeatureC which needs state from useFeatureA).

  • If their is a circular feature dependency, then one dependency should only be in a DOM event callback to break the chain (for instance if useFeatureA needs a useFeatureB state in the render and useFeatureB needs a useFeatureA method but in a GridEvents.rowClick event, then it should not be an issue as long as the handleRowClick callback does not use any selectors executed in the render.

  • The difficult scenarios occurs when useFeatureA needs something from useFeatureB which is used in a method exposed by useFeatureA and useFeatureB needs something from useFeatureA in the render. Because it is way harder to track if the method exposed by useFeatureA is ever called synchronously. In that scenario, the best thing is to try to execute useFeatureB just after useFeatureA so that any hook after useFeatureB is also after useFeatureA

But if we can have almost no circular feature dependency and therefore apply a perfect hook execution order. Then it would be a lot easier.

@flaviendelangle flaviendelangle self-assigned this Aug 10, 2021
@flaviendelangle flaviendelangle marked this pull request as draft August 10, 2021 16:42
@flaviendelangle flaviendelangle marked this pull request as ready for review August 11, 2021 08:07
Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documenting the dependencies of each hook sounds like a great idea

@oliviertassinari oliviertassinari added component: data grid This is the name of the generic UI component, not the React module! core Infrastructure work going on behind the scenes docs Improvements or additions to the documentation labels Aug 14, 2021
@flaviendelangle flaviendelangle changed the base branch from master to next August 26, 2021 07:56
@flaviendelangle flaviendelangle merged commit ce914c8 into mui:next Aug 30, 2021
@flaviendelangle flaviendelangle deleted the hook-requires branch August 30, 2021 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! core Infrastructure work going on behind the scenes docs Improvements or additions to the documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants