Search code examples
reactjsreduxreact-reduxreact-hooks

How do I select/unselect in redux?


I have a list of trainers and I have to select and unselect them as a toggle button. Below is the code to do so but I don't know how to implement it correctly.

Action:

 const selectTrainer = (selected) => ({
    type: 'SELECT_TRAINER',
    payload: {
      
      selected: selected
    }
  });

Trainer:

function Trainer(props) {

  const [addBtnToggle, setBtnToggle] = useState(false);

  return (
    <>
      
        {addBtnToggle ?  <button onClick={() => setBtnToggle(!addBtnToggle)}>Selected</button> : <button onClick={() => setBtnToggle(!addBtnToggle)}>Select</button> }
        {addBtnToggle && <Select />}
    </>
  );
}

export default Trainer;

Reducer:

import {v4 as uuid} from 'uuid';
const initalState = [
  {
    id: uuid(),
    name: '',
    team: [],
    selected: false
  }
];

let copyState = null;
let index = 0;

const trainerReducer = (state = initalState, action) => {
  const {type, payload} = action
   
    case "SELECT_TRAINER":
      return state.map((trainer) => 
        trainer.id === payload.id ? trainer.selected = true : trainer
      );


    default:
      return state;
  }
};

export default trainerReducer;

Select:

function Select() {
    const dispatch = useDispatch();
    const allTrainers = useSelector((state) => state.trainers);
    allTrainers.map(a => a.selected===true );
  
    const arr = []
    arr.push(props.data.name)
    console.log(result)
    dispatch(actions.selectTrainer());
    return null
}
export default Select;

How do I add a toggle to unselect a particular item and how do I unselect the already selected item when I want to select another item?


Solution

  • When using what I refer to as legacy reducers (i.e. pre-redux-toolkit) you need to apply the Immutable Update Pattern. The reducer function is correctly creating a shallow copy of the state array, but it also needs to shallow copy any nested state it is updating. In this case it should create a new "trainer" object reference with the copied/updated properties.

    Example:

    const initialState = [
      {
        id: uuid(),
        name: '',
        team: [],
        selected: false
      }
    ];
    
    const trainerReducer = (state = initialState, action) => {
      const {type, payload} = action
      
      switch(type) { 
        case "SELECT_TRAINER":
          return state.map((trainer) => // <-- shallow copy array
            trainer.id === payload.id
              ? {                 // <-- new object reference
                  ...trainer,     // <-- shallow copy trainer properties
                  selected: true, // <-- overwrite property with new value
                } 
              : trainer
          );
    
        ... other cases...
    
        default:
          return state;
      }
    };
    

    How do I add a toggle to unselect a particular item and how do I unselect the already selected item when I want to select another item?

    This sounds like you want only a single selected trainer. You can update the reducer case to also check if the current trainer object is selected and deselect it.

    case "SELECT_TRAINER":
      return state.map((trainer) =>
        trainer.id === payload.id
          ? { ...trainer, selected: true } 
          : trainer.selected
            ? { ...trainer, selected: false }
            : trainer
        );
    

    or more DRY

    case "SELECT_TRAINER":
      return state.map((trainer) =>
        trainer.id === payload.id || trainer.selected
          ? { ...trainer, selected: trainer.id === payload.id } 
          : trainer
        );
    

    You will need to dispatch a trainer's id with the action. For this I suggest moving the redux logic from the Select component into the Trainer component so it computes the selected trainer value and directly dispatches the selectedTrainer action with the current trainer's id.

    function Trainer(props) {
      const dispatch = useDispatch();
      const allTrainers = useSelector((state) => state.trainers);
      const isSelected = allTrainers.some(trainer => trainer.selected);
    
      return (
        <button onClick={() => dispatch(actions.selectTrainer(props.id))}>
          {isSelected ? "Selected" : "Select"}
        </button>
      );
    }
    

    To avoid needing to shallow copy the trainers array it may make more sense to just store a selected trainer id as a separate chunk of state.

    Example:

    const initialState = {
      trainers: [
        {
          id: uuid(),
          name: '',
          team: [],
        }
      ],
      selected: null
    };
    
    const trainerReducer = (state = initialState, action) => {
      const {type, payload} = action
      
      switch(type) { 
        case "SELECT_TRAINER":
          return {
            ...state,
            selected: state.selected === payload.id ? null : payload.id
          }
    
        ... other cases...
    
        default:
          return state;
      }
    };
    

    ...

    function Trainer(props) {
      const dispatch = useDispatch();
      const selected = useSelector((state) => state.trainers.selected);
    
      return (
        <button onClick={() => dispatch(actions.selectTrainer(props.id))}>
          {selected === props.id ? "Selected" : "Select"}
        </button>
      );
    }