Can you have slices inside slices? How to share state logic between slices?

Viewed 46

EDIT: For complete reference, Im trying to do this with Redux

https://codesandbox.io/s/github/SenBoyer/codm-tracker-unfinished

For quick visual reference:

https://jsfiddle.net/59r31Lmt/

Im trying to make it so when you click a camo square it becomes active, when you click all the camo squares, the gold unlocks, and when you unlock gold for ALL of the guns, the diamond "unlocks"/becomes active for all of them.

This is the slice I'm using

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  sandIsActive: false,
  jungleIsActive: false,
  splinterIsActive: false,
  tigerIsActive: false,
  dragonIsActive: false,
  reptileIsActive: false,
  goldIsActive: false,
  diamondIsActive: false,
};

const camoBarSlice = createSlice({
  name: "camoBar",
  initialState,
  reducers: {
    setSandActive: (state, action) => {
      return {
        ...state,
        sandIsActive: !state.isActive,
      };
    },
    setJungleActive: (state, action) => {
      return {
        ...state,
        jungleIsActive: !state.isActive,
      };
    },
    setSplinterActive: (state, action) => {
      return {
        ...state,
        splinterIsActive: !state.isActive,
      };
    },
    setTigerActive: (state, action) => {
      return {
        ...state,
        tigerIsActive: !state.isActive,
      };
    },
    setDragonActive: (state, action) => {
      return {
        ...state,
        dragonIsActive: !state.isActive,
      };
    },
    setReptileActive: (state, action) => {
      return {
        ...state,
        reptileIsActive: !state.isActive,
      };
    },
    setGoldActive: (state, action) => {
      return {
        ...state,
        goldIsActive: !state.isActive,
      };
    },
  },
});

So when

  sandIsActive: true,
  jungleIsActive: true,
  splinterIsActive: true,
  tigerIsActive: true,
  dragonIsActive: true,
  reptileIsActive: true,

setGoldActive() runs and:

goldIsActive: true

then when goldIsActive is true for all 20ish guns, setDiamondActive() runs and:

diamondIsActive: true

for every gun.

How do I share the state logic between all the guns so that it will know to run the function setDiamondActive when they all become active?

Do I have to make an individual slice for each gun with the same logic? Then what though? How do I pass the logic?

Do I have to add a state value for every single square on the whole website because it's not possible to share state values across other slices? You need to keep it all in one slice? So it would have to be something like:

const initialState = {
  ak47SandIsActive: false,
  ak47JungleIsActive: false,
  ak47SplinterIsActive: false,
  ak47TigerIsActive: false,
  ak47DragonIsActive: false,
  ak47ReptileIsActive: false,
  ak47GoldIsActive: false,
  ak47DiamondIsActive: false,

  m13SandIsActive: false,
  m13JungleIsActive: false,
  m13SplinterIsActive: false,
  m13TigerIsActive: false,
  m13DragonIsActive: false,
  m13ReptileIsActive: false,
  m13GoldIsActive: false,
  m13DiamondIsActive: false,
};

then set an onClick on each and every square with the relevant dispatch action?

Can I put a slice inside another slice? So that I can have an "outer reducer" that controls the diamond square, while an inside reducer controls the entirety of the gun logic? Is there anything close to that or that could accomplish the same thing?

Basically how do I use redux to keep track of the state here? The logic in the slice I posted works for one gun at a time but how do I use it to control the logic of 20+ guns here?

1 Answers

I'll just make a few assumptions about your code:

  • You want to know when each gun is active.
  • You want to know when some guns are active/inactive.
  • You have complex interactions between these guns' states.

To that end, my advice is to combine everything in one slice. If you need to access data from one slice in another, then, realistically, it's one slice, because the scope is the same. Think of slices as scopes.

First, we need a function to determine when diamond should be toggled on:

export const isDiamondActive = (state) => {
  const targetKeys = ['sand', 'jungle', reptile'...];
  
  targetKeys.forEach(targetKey => {

    if(state[targetKey] === false) {
      return false;
    }
  }

  return true;
}

This function simply loops through all of the keys that, if they're all true, should tell us that diamond should also be active. Remember this function.

Also, with RTK, you no longer need to worry about copying "past state" as you had to with Redux. Just write your logic and don't think about that, so:

    setReptileActive: (state, action) => {
      return {
        ...state,
        reptileIsActive: !state.isActive,
      };
    },

becomes:

    setReptileActive: (state, action) => {
      state[reptileIsActive] = !state[reptileIsActive];
    },

Or, better yet, delete all of the above reducers and refactor it in one function:

toggledGunStatus: (state, action) => {
  const { id } = action;


  //If the did we were provided isn't in the state, stop.
  if(!(id in state)) {
    return;
  }

  state[id] = !state[id];
}

With all of this in mind, we have two options. I'd argue that the check for isDiamondActive should be made inside the toggleGunStatus reducer, because they're closely tied together. So, a simple:

const diamondActive = isDiamondActive(state);

if(diamondActive) {
  state['diamond'] = diamondActive;
}

However, things can get messy when we're talking about complex logic, so, perhaps you'd want a listener middleware setup for this - it will simply listen for the toggledGunStatus action and, in response to it, run that isDiamondActive check and dispatch/change state further.


As a side-note, it strikes me that your system will be a bit more complex. Each gun will hold a lot of data, not just the isActive flag. Consider looking into the entity adapter, it offers pre-built CRUD operations for collections of items. In essence, your slice should be an array of objects, where each object is a gun. If you were using TS, you could elegantly define all of this as:

export type Gun = {
  id: string;
  isActive: boolean;
}

export const entityAdapter = createEntityAdapter<Gun>();

..

const initialState: InitialState /* complex, can ignore */ = {
  guns: entityAdapter.getInitialState();
}

And now, every time you work with a selector or so, you're working with a collection. Much cleaner and "future-proof".

Related