Suppose I have a long list of playerData
in my redux store, and some players' data gets updated very often. Let's say I have a Player
component and inside of it I want to access that player's specific data. To do so, I would need to access a specific index of the playerData
array.
Selector functions are supposed to be pure functions, and my understanding is that pure functions cannot alter their return values based on outside data. Am I correct in believing that the following three examples are therefore invalid?
//A
const Player = ({ index }) => {
const thisPlayerData = useSelector(state => state.playerDataArray[index]);
return <div>{thisPlayerData.name}</div>
}
//B
const ActivePlayer = ({}) => {
const [activePlayerIndex, setActivePlayerIndex] = useState(0);
const activePlayerData = useSelector(state => state.playerDataArray[activePlayerIndex]);
return <div>{activePlayerData.name}</div>
}
//C
const WinnerPlayer = ({}) => {
const winningPlayerIndex = useSelector(state => state.winningPlayerIndex);
const winnerPlayerData = useSelector(state => state.playerData[winningPlayerIndex]);
return <div>{winnerPlayerData.name}</div>
}
If those three are invalid, the only way I can think to access a player's data is by selecting the entire playerData
array and then getting a specific index afterwards.
// Using example B as a jumping off point...
const ActivePlayer = ({}) => {
const [activePlayerIndex, setActivePlayerIndex] = useState(0);
const allPlayerDatas = useSelector(state => state.playerDataArray);
const activePlayerData = allPlayerData[activePlayerIndex];
return <div>{activePlayerData.name}</div>
}
I would like to avoid this if possible, as that would (if I'm understanding correctly) cause the component to rerender every single time any playerData is changed, not just when the playerData it's trying to display is changed.
Ideally, I would like the component only to rerender when the index
or the playerDataArray[index]
changes. Is there a way to accomplish that?
Suppose I have a long list of playerData
in my redux store, and some players' data gets updated very often. Let's say I have a Player
component and inside of it I want to access that player's specific data. To do so, I would need to access a specific index of the playerData
array.
Selector functions are supposed to be pure functions, and my understanding is that pure functions cannot alter their return values based on outside data. Am I correct in believing that the following three examples are therefore invalid?
//A
const Player = ({ index }) => {
const thisPlayerData = useSelector(state => state.playerDataArray[index]);
return <div>{thisPlayerData.name}</div>
}
//B
const ActivePlayer = ({}) => {
const [activePlayerIndex, setActivePlayerIndex] = useState(0);
const activePlayerData = useSelector(state => state.playerDataArray[activePlayerIndex]);
return <div>{activePlayerData.name}</div>
}
//C
const WinnerPlayer = ({}) => {
const winningPlayerIndex = useSelector(state => state.winningPlayerIndex);
const winnerPlayerData = useSelector(state => state.playerData[winningPlayerIndex]);
return <div>{winnerPlayerData.name}</div>
}
If those three are invalid, the only way I can think to access a player's data is by selecting the entire playerData
array and then getting a specific index afterwards.
// Using example B as a jumping off point...
const ActivePlayer = ({}) => {
const [activePlayerIndex, setActivePlayerIndex] = useState(0);
const allPlayerDatas = useSelector(state => state.playerDataArray);
const activePlayerData = allPlayerData[activePlayerIndex];
return <div>{activePlayerData.name}</div>
}
I would like to avoid this if possible, as that would (if I'm understanding correctly) cause the component to rerender every single time any playerData is changed, not just when the playerData it's trying to display is changed.
Ideally, I would like the component only to rerender when the index
or the playerDataArray[index]
changes. Is there a way to accomplish that?
2 Answers
Reset to default 3Pure function means that calling the function with the same arguments, would always return the same result, and that the function doesn't have side effects. Calling a selector with the current state, and a prop doesn't make it impure - if the same state and prop are used again, the return value would be identical.
All your examples are valid. In the redux docs for useSelecor()
examples you have the "Using props via closure to determine what to extract" example:
export const TodoListItem = (props) => {
const todo = useSelector((state) => state.todos[props.id])
return <div>{todo.text}</div>
}
In your scenario both values actually come from the state, so you can generate a single selector. Create 2 simple selectors, and combine them:
const getWinningPlayerIndex = state => state.winningPlayerIndex;
const getPlayerData = state => state.playerData;
const getWinnerData = state => getPlayerData(state)[getWinningPlayerIndex(state)];
const WinnerPlayer = () => {
const winnerPlayerData = useSelector(getWinnerData);
return <div>{winnerPlayerData.name}</div>
}
If you want to avoid exposing the internal shape of your application state to your components, it's useful to define your selectors separately. i.e.
function playerDataSelector(state, { index }) {
return state.playerDataArray[index];
}
function winningPlayerDataSelector(state) {
const { playerData, winningPlayerIndex } = state;
return playerData[winningPlayerIndex];
}
Or you can use reselect to define your selectors. The createSelector
function takes two arguments: an array of dependencies, and a result function. The dependencies are used as a caching mechanism; the result is only recalculated if the values of the dependencies change. It's not strictly necessary in this instance since your result functions have O(1) time, but it becomes very useful with more complex result functions.
import { createSelector } from 'reselect';
const playerDataSelector = createSelector(
[
(state) => state.playerDataArray,
(state, props) => props.index,
],
(playerDataArray, index) => playerDataArray[index],
);
const winningPlayerDataSelector = createSelector(
[
(state) => state.playerDataArray,
(state) => state.winningPlayerIndex,
],
(playerDataArray, winningPlayerIndex) => playerDataArray[winningPlayerIndex],
);
Finally plug the selectors into your code:
//A
const Player = ({ index }) => {
const thisPlayerData = useSelector((state) => playerDataSelector(state, { index }));
return <div>{thisPlayerData.name}</div>
}
//B
const ActivePlayer = ({}) => {
const [activePlayerIndex, setActivePlayerIndex] = useState(0);
const thisPlayerData = useSelector((state) => playerDataSelector(state, { index: activePlayerIndex }));
return <div>{activePlayerData.name}</div>
}
//C
const WinnerPlayer = ({}) => {
const winnerPlayerData = useSelector(winningPlayerDataSelector);
return <div>{winnerPlayerData.name}</div>
}