facebook / react

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

Feature Request: Selectively Retrieve Values from Context and update components accordingly #17777

Closed GasimGasimzada closed 4 years ago

GasimGasimzada commented 4 years ago

Do you want to request a feature or report a bug?

Feature

What is the current behavior?

Currently, any changes in the context will update all components.

What is the expected behavior?

Currently, when subscribing to context, if any value in the context changes, all consumers will be updated:

const value = useContext(MyContext);

My suggestion is the following: optionally, allow receiving specific values from context values and only update the components if the returned values change. Here is an example:

const data = useContext(MyContext, value => value.data); // value = context value

When the second function argument is defined, only the returned value from the function will be compared and accessed. This will simplify a lot of workflows where multiple contexts are used for multiple values in order to reduce the number of context updates. Here is an example from Hooks FAQ (modified a little bit):

function TodosApp() {
  // Note: `dispatch` won't change between re-renders
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosData.Provider value={todos}>
      <TodosDispatch.Provider value={dispatch}>
        <DeepTree todos={todos} />
      </TodosDispatch.Provider>
    </TodosData.Provider>
  );
}

function DeepChild(props) {
  // If we want to perform an action, we can get dispatch from context.
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}

function AnotherDeepChild(props) {
  const data = useContext(TodosData);
  ...
}

With the proposed API addition, we can just use the same context to do retrieve two different values:

function TodosApp() {
  // Note: `dispatch` won't change between re-renders
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosContext.Provider value={{ todos, dispatch }}>
        <DeepTree todos={todos} />
    </TodosContext.Provider>
  );
}

function DeepChild(props) {
  const dispatch = useContext(TodosContext, value => value.dispatch);
}

function AnotherDeepChild(props) {
  const todos = useContext(TodosContext, value => value.todos);
}

Another useful scenario for this addition is dynamically selecting items from a centralized store based on context. If a developer needs to selectively retrieve specific values from an object, they can do it very easily. Something similar to Redux' useSelector but is part of a normal Context Flow:

function UserInfoApp() {
  // Note: `dispatch` won't change between re-renders
  const [user, dispatch] = useReducer(userReducer, { name: ..., dob: ..., active: ... });

  return (
    <UserInfoContext.Provider value={user}>
        <DeepTree todos={todos} />
    </UserInfoContext.Provider>
  );
}

function UserInfoTable(props) {
  const name = useContext(UserInfoContext, value => value.name);
  const dob = useContext(UserInfoContext, value => value.dob);
}

function UserActiveTracker(props) {
  const todos = useContext(UserInfoContext, value => value.active);
}

Now, if active value is changed, only UserActiveTracker will be updated. If name or dob is changed, only UserInfoTable will be activated.

The API can also be implemented for Context.Consumer component:

<MyContext.Consumer selector={value => value.active}> ... </MyContext.Consumer>

and static contextType static class variable:

class MyComponent extends React.Component {
  static contextType = MyContext;
  static contextSelector = value => value.active;

  render() {
    const val = this.context.active;
    const wrongVal = this.context.name; // = undefined

    return ...
  }
}
bvaughn commented 4 years ago

This sort of request should follow our RFC process: https://github.com/reactjs/rfcs/

In this case, there already are some related RFCs:

I suggest you either create a new alternative RFC or collaborate with the existing open one. :smile: (I'm going to close this issue out in the meanwhile)

For what it's worth, Dan brought up this issue a while ago (#14110) and there are some current mitigation strategies we recommend. He's outlined them here: https://github.com/facebook/react/issues/15156#issuecomment-474590693