Skip to content
Advertisement

React/Redux Toolkit render issue when deleting state from an array

Problem: Component render starts to drift from actual state

Desired Output: Component render matches state.

So. I’m going to give a bit of a high-level overview with pseudocode as this issue is quite complex, and then I’ll show the code.

I have a main form, and this form has an array of filter-states that are renderable in their own components. These filter-states are a one-to-many relationship with the form. The form has-many filter-states.

form: {
  filters: [
    filter1,
    filter2
  ]
}

Say you want to remove an item from the state, you would do something like so in the reducer (redux)

state.form.filters.filter(f => f.id != action.payload.id)

All good. The state is updated.

Say, you want to render this state, you would do something like so:

// component code ommited, but say you get your form state from redux into the component
formState.filters.map(filter => <FilterComponent filter={filter}/>

All good. your filters are being injected into the component and everyone is happy

Now. This is where it gets weird pretty quickly.

There is a button on my FilterComponent, that says delete. This delete button goes to the reducer, runs the code to delete the filter from the formstate (as you saw above), and yes, it DOES work. The state gets updated, BUT, the UI (the array of components) starts to drift from the state. The UI shows previously deleted states, and states that should be persisted are not shown (but in the redux tab on chrome, the state is CORRECT…!)

The UI acts as if the array of states is being pop()‘d; no matter how you remove the states, it will remove the final state in component render.

Now, for the code.

// This takes a list of filters from the form state and loads them into individual form components
const Filters: NextPage<Filters> = () => {
  const formState = useSelector((state: any) => state.form.formState)

  // In the hope that state change will force reload components, but no avail
  useEffect(() => {
    console.log("something has been reloaded")
  }, [formState])

  return (
    <>
      {formState.form.map((filter, i) => {
        return <FilterForm defaultState={filter} key={i} index={i} />
      })}
    </>
  );
};

export default Filters;

The form for these individual states: Please note, this is obviously redacted a lot but the integral logic is included

const FilterForm: NextPage<FilterFormProps> = ({ defaultState, index }) => {
  const formState = useSelector((state: any) => state.form.formState)
  // Local component state; there are multiple forms so the state should be localised
  const [FilterState, setFilterState] = useState(defaultState)
  const handleDelete = (e) => {
    dispatch(deleteFilter(filterState.id))
  }
  const updateParentState = async () => {
    dispatch(updateForm(filterState))
  }

  useEffect(() => {
    updateParentState()
  }, [filterState])

  return (
    <CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} name={filterState.id} />
    <Input
      name="filter_value"
      onChange={handleOnChange} // does standard jazz
      value={filterState.filter_value} // standard jazz again
    />
  )
}

Now what happens is this: if I click delete, redux updates the correct state, but the components display the deleted state input. Ie, take the following:

filter1: {filter_value: "one"}
filter2: {filter_value: "two"}
filter3: {filter_value: "three"}

these filters are rendered in their own forms.

Say, I click delete on filter1.

filter1 will be deleted from redux, but the UI will show two forms: one for filter1 and one for filter2.

This drift from UI to state baffles me. Obviously I am doing something wrong, can someone spot what it is?!

Advertisement

Answer

So, I fixed the issue.

As it turns out, there isnt really an explanation for why the above behaved as it does, but it does warrant for a better implementation.

The issue was as follows; the redux state was conflicting with the local state of the rendered components it was injected in. Why it did, is another story. Somehow, while injecting the redux state into the component and assigning it to the local state, the states went a bit haywire and drifted apart.

The solution was to get rid of the local state (filterState), the updateParentState function call and rather to update the localised state directly through the parent state that it resides in.

The new component looked something like the following:

const FilterForm: NextPage<FilterFormProps> = ({ state, index }) => {
  const handleDelete = (e) => {
    dispatch(deleteFilter(filterState.id))
  }

  const handleChange = (e) => {
    dispatch(updateFormFilterState({ ...state, [e.target.name]: e.target.value }))
  }

  return (
    <CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} />
    <Input
      name="filter_value"
      onChange={handleChange} 
      value={state.filter_value} 
    />
  )
}

Hope this answer helps someone with the same issue as me.

User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement