I came across this behavior while working on this epic react lesson, and after re-reading the react docs on both forwardRef and HOCs cannot figure out why this would be happening.
I've cut the epic react lesson down below to demonstrate what I am seeing. Basically, it's an AppProvider component providing access to the global state through context, then the withStateSlice HOC retrieves the correct slice of state and injects it into the state prop of the Cell components.
The weird behavior I'm seeing is when the withStateSlice HOC calls forwardRef on the wrapped component:
return React.memo(React.forwardRef(Wrapper))
the profiler shows that only the one Cell that was clicked actually rendered.

If I remove forwardRef from the above so we are left with this:
return React.memo(Wrapper)
then I am seeing all the Cell HOC components rendering, which from what I understand of context should be the expected behavior.

From what I've read this it doesn't seem like forwardRef should affect rendering when context changes but I could be wrong. This is with React 17.0.2 and I am seeing the same behavior in development and production builds (with profiling turned on using: react-scripts build --profile
import * as React from 'react'
const AppStateContext = React.createContext()
const AppDispatchContext = React.createContext()
function appReducer(state, action) {
switch (action.type) {
case 'UPDATE_CELL': {
const newState = [...state];
newState[action.i] = state[action.i] + 1;
return newState
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function AppProvider({children}) {
const [state, dispatch] = React.useReducer(appReducer, [5, 5, 5, 5, 5, 5])
return (
<AppStateContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppStateContext.Provider>
)
}
function useAppState() {
const context = React.useContext(AppStateContext)
if (!context) {
throw new Error('useAppState must be used within the AppProvider')
}
return context
}
function useAppDispatch() {
const context = React.useContext(AppDispatchContext)
if (!context) {
throw new Error('useAppDispatch must be used within the AppProvider')
}
return context
}
function fibonacci(n) {
return n < 1 ? 0
: n <= 2 ? 1
: fibonacci(n - 1) + fibonacci(n - 2)
}
function withStateSlice(Comp, slice) {
const MemoComp = React.memo(Comp)
function Wrapper(props, ref) {
const state = useAppState()
return <MemoComp ref={ref} state={slice(state, props)} {...props} />
}
Wrapper.displayName = `withStateSlice(${Comp.displayName || Comp.name})`
return React.memo(React.forwardRef(Wrapper)) // return React.memo(Wrapper)
}
function Cell({state: cellValue, i}) {
const dispatch = useAppDispatch()
const handleClick = () => dispatch({type: 'UPDATE_CELL', i})
// Artificial slowdown for better visibility in profiler
for (let n = 10; n < 20; n++) {
fibonacci(n);
}
return (
<button
className="cell"
onClick={handleClick}
>
{Math.floor(cellValue)}
</button>
)
}
Cell = withStateSlice(Cell, (state, { i }) => state[i])
function App() {
return (
<div className="grid-app">
<div>
<AppProvider>
<Cell i={0} />
<Cell i={1} />
<Cell i={2} />
<Cell i={3} />
<Cell i={4} />
<Cell i={5} />
</AppProvider>
</div>
</div>
)
}
export default App