Search code examples
javascriptreactjsredux

How to change Redux state from a path string?


I have initial state like this:

const initialState = {
  array: [
    {
      key: "value",
      obj: {
        key1: "value",
        key2: "value",
      },
      array: [
        {
          key: "value",
          obj: {
            key1: "value",
            key2: "value",
          },
        }
      ]
    },
    {
      key: "value",
      obj: {
        key1: "value",
        key2: "value",
      },
    },
    {
      key: "value",
      obj: {
        key1: "value",
        key2: "value",
      },
    },
  ],
  path: "",
  value: ""
};

Reducer:

export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "SET_PATH":
      return {
        ...state,
        path: action.path
      };

    case "SET_NEW_VALUE":
      return {
        ...state,
        newValue: action.value
      };

    case "SET_NEW_BUILD":
      //What next?

    default:
      return state
  }
};

Action creators:

const setPath = (path) => ({type: "SET_PATH", path});
const setNewValue = (value) => ({type: "SET_NEW_VALUE", value});
const setNewBuild = (path, value) => ({type: "SET_NEW_BUILD", path, value});

And i need to change this state after this dispatch using a path string and new value.

dispatch(setNewBuild("array[0].obj.key1", "newValue");

Also the value can have form like this "obj: {key1: "newValue", key2: "newValue"}" hence will be created a new object.

How can i do this?


Solution

  • Here is an example using the set helper:

    const REMOVE = () => REMOVE;
    //helper to get state values
    const get = (object, path, defaultValue) => {
      const recur = (current, path, defaultValue) => {
        if (current === undefined) {
          return defaultValue;
        }
        if (path.length === 0) {
          return current;
        }
        return recur(
          current[path[0]],
          path.slice(1),
          defaultValue
        );
      };
      return recur(object, path, defaultValue);
    };
    //helper to set state values
    const set = (object, path, callback) => {
      const setKey = (current, key, value) => {
        if (Array.isArray(current)) {
          return value === REMOVE
            ? current.filter((_, i) => key !== i)
            : current.map((c, i) => (i === key ? value : c));
        }
        return value === REMOVE
          ? Object.entries(current).reduce((result, [k, v]) => {
              if (k !== key) {
                result[k] = v;
              }
              return result;
            }, {})
          : { ...current, [key]: value };
      };
      const recur = (current, path, newValue) => {
        if (path.length === 1) {
          return setKey(current, path[0], newValue);
        }
        return setKey(
          current,
          path[0],
          recur(current[path[0]], path.slice(1), newValue)
        );
      };
      const oldValue = get(object, path);
      const newValue = callback(oldValue);
      if (oldValue === newValue) {
        return object;
      }
      return recur(object, path, newValue);
    };
    const { Provider, useDispatch, useSelector } = ReactRedux;
    const { createStore } = Redux;
    
    //action
    const setNewBuild = (path, value) => ({
      type: 'SET_NEW_BUILD',
      path,
      value,
    });
    const initialState = {
      array: [
        {
          key: 'value',
          obj: {
            key1: 'value',
            key2: 'value',
          },
        },
      ],
      path: '',
      value: '',
    };
    const reducer = (state = initialState, action) => {
      const { type } = action;
      if (type === 'SET_NEW_BUILD') {
        const { path, value } = action;
        return set(state, path, () => value);
      }
      return state;
    };
    const store = createStore(
      reducer,
      initialState,
      window.__REDUX_DEVTOOLS_EXTENSION__ &&
        window.__REDUX_DEVTOOLS_EXTENSION__()
    );
    const App = () => {
      const state = useSelector((x) => x);
      const dispatch = useDispatch();
      return (
        <div>
          <button
            onClick={() =>
              dispatch(
                setNewBuild(
                  ['array', 0, 'obj', 'key1'],
                  'new value key 1'
                )
              )
            }
          >
            change array 0 obj key1
          </button>
          <button
            onClick={() =>
              dispatch(
                setNewBuild(['array', 0, 'obj'], {
                  key1: 'change both key1',
                  key2: 'change both key2',
                })
              )
            }
          >
            change array 0 obj
          </button>
          <pre>{JSON.stringify(state, undefined, 2)}</pre>
        </div>
      );
    };
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
    
    
    <div id="root"></div>

    The important bits are:

    <button
      onClick={() =>
        dispatch(
          // path is an array
          setNewBuild(['array', 0, 'obj'], {
            key1: 'change both key1',
            key2: 'change both key2',
          })
        )
      }
    >
      change array 0 obj
    </button>
    

    And in the reducer:

    const { type } = action;
    if (type === 'SET_NEW_BUILD') {
      const { path, value } = action;
      return set(state, path, () => value);
    }