0

I am self-training in React, and I have a problem that I can't find any solution, I hope you can help me.

Here is my problem: I have a component that contains a list of products, this list is managed with a reducer (add, delete and modify). This list can contain simple items (which are components), or other item lists (a kind of package), these packages are also components whose content is also managed by another reducer.

here is an example of what my list is, which can contain items, or lists of items: list example

The desired behavior is this:

  1. I add a package to my product list with the reducer.
  2. The state of the list is updated
  3. I add an item to this package with the package reducer, automatically the state of the package is updated, and thanks to a useEffect also updates the product list
  4. I remove the item from the package with the reducer, the state of the package is updated, and the list is updated with the useEffect. If package.length (I simplify) < 1 then I delete the package from my main list thanks to the reducer of the main list

it works perfectly, until I add a second package [package1, package2], both packages have an item : [[item of pkg1], [item of pkg2]]. I delete the item of package 1, automatically package 1 is deleted from my main list, and I should get : [[item of pkg2]] except that I get: [[]] My second package is also empty.

Here is what I noticed:

  1. the content of my second package = the initial state of the package reducer (if I initialize it with an item, then it will be there when package 1 is deleted)
  2. If I do the same thing, but deleting the item of package 2, package 2 is deleted, and package 1 is unchanged (desired behavior)
  3. If I remove the condition => If package.length < 1 I delete the package from the list, the package 2 is not modified.
  • The full code is available on my github
  • you can find reducer on /src/hook : main list reducer is => LabelsReducer. Package (named group on the project) reducer is => GroupReducer.

The item component is on /src/App/FilterLabel => Label component. The group component is on /src/App/FilterLabel => Group component.

LabelsReducer :

import { useEffect, useReducer, useState } from "react";

function reducer(state, action) {
    console.log('FILTER REDUCER', action.type, action)
    switch (action.type) {
        case 'ADD_FILTER':
            action.payload.id = action.payload.id + '-' + action.uuid
            if (state.selectedFilters.filter(filter => filter.id == action.payload.id).length > 0) {
                action.onError('FILTER REDUCER - ' + action.type + ' : Erreur lors de l\'ajout, présence de doublons')
                return state
            }
            return { selectedFilters: [...state.selectedFilters, action.payload] }

        case 'DELETE_FILTER':
            if (state.selectedFilters.filter(filter => filter.id == action.payload).length > 1) {
                action.onError('FILTER REDUCER - ' + action.type + ' : Erreur lors de la suppression, présence de doublons')
                return state
            }

            console.log('BEFORE ', state.selectedFilters)
            return { selectedFilters: [...state.selectedFilters.filter(filter => filter.id != action.payload)] }

        case 'UPDATE_FILTER':
            let line = state.selectedFilters.filter(filter => filter.id == action.payload.id)
            let index = state.selectedFilters.indexOf(line[0])
            if (index == -1) {
                action.onError('FILTER REDUCER - ' + action.type + " : Item non trouvé")
                return state
            }
            state.selectedFilters[index] = action.payload
            return { ...state }
        default:
            return state
    }
}

export default function LabelsReducer(setError) {
    const [state, dispatch] = useReducer(reducer, {
        selectedFilters: []
    })

    // useEffect(() => {
    //     console.warn(state.selectedFilters)
    // }, [state])

    function onError(message) {
        setError({ message: message })
    }

    const [id, setId] = useState(0)
    function newId() {
        let newId = id.valueOf()
        setId(id + 1)
        return newId
    }

    return {
        selectedFilters: state.selectedFilters,
        addSelectedFilter: (filter) => {
            dispatch({ type: 'ADD_FILTER', payload: filter, onError: onError, uuid: newId() })
        },
        deleteSelectedFilter: function (id) {
            dispatch({ type: 'DELETE_FILTER', payload: id, onError: onError })
        },
        updateSelectedFilter: (filter) => {
            dispatch({ type: 'UPDATE_FILTER', payload: filter, onError: onError })
        }
    }
}

GroupReducer :

import React, { useEffect, useReducer, useState } from "react"

function reducer(state, action) {
    console.log('GROUP REDUCER', action.type, action)
    switch (action.type) {

        case 'ADD_FILTER':
            action.payload.id = action.payload.id + '-' + action.uuid
            if (state.group.filter(filter => filter.id == action.payload.id).length > 0) {
                console.warn('DOUBLON : id => ', action.payload.id)
            }
            return { group: [...state.group, action.payload] }

        case 'DELETE_FILTER':
            return { group: state.group.filter(contrat => contrat.id !== action.payload) }
    }
}

export default function GroupReducer() {
    const [state, dispatch] = useReducer(reducer, {
        group: [{ label: 'INIT', id: 2, type: 'package', contrat: '70010', canSelected: true, participants: [] }]
    })

    const [id, setId] = useState(0)
    function newId() {
        let newId = id.valueOf()
        setId(id + 1)
        return newId
    }

    return {
        groupContract: state.group,
        addContract: function (filters) {
            dispatch({ type: 'ADD_FILTER', payload: filters, uuid: newId() })
        },
        deleteContract: function (filterId) {
            dispatch({ type: 'DELETE_FILTER', payload: filterId })
        }
    }
}

and finally, Labels (item) and group components :

import React, { useEffect } from 'react'
import GroupReducer from '../Hook/GroupReducer'

export function Label({ filter, onDelete, updateSelectedFilter }) {

    function deleteFilter() {
        onDelete(filter.id)
    }

    //updateSelectedFilter to implement on second svg to add new participants
    return <span className='badge text-bg-primary ms-1'>{filter.label}
        {filter.participants.length != 0 ? filter.participants.map((part, index) => {
            return <span style={{ color: 'cyan' }} key={index} className='ms-2'>{part}</span>
        }) : null}

        <svg style={{ cursor: 'pointer' }} xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" className="bi bi-person-fill ms-2" viewBox="0 0 16 16">
            <path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
        </svg>

        <svg onClick={() => deleteFilter()} style={{ cursor: 'pointer' }} xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" className="bi bi-x-circle-fill ms-2" viewBox="0 0 16 16">
            <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
        </svg>
    </span>
}

export function GroupLabel({ group, onDelete, onUpdate }) {

    const {
        groupContract,
        addContract,
        deleteContract
    } = GroupReducer()

    useEffect(() => {
        group.contrats = groupContract
        onUpdate(group)
    }, [groupContract])


    async function deleteSelectedFilter(contractId) {
        await deleteContract(contractId)
        group.contrats < 1 && onDelete(group.id)
    }

    function updateSelectedFilter() {
        console.log('update filter in group')
    }

    function TEST_addContract() {
        let contrat = { label: 'Package 70010', id: 2, type: 'package', contrat: '70010', canSelected: true, participants: [] }
        addContract(contrat)
    }

    return <span className='contractGroup ms-1'>
        {group.contrats.map((filter, index) => {
            return <Label key={`${group.id}-${filter.id}-${index}`}
                filter={filter}
                onDelete={deleteSelectedFilter}
                updateSelectedFilter={updateSelectedFilter}
            />
        })}
        <button onClick={() => TEST_addContract()}>add</button>
    </span>
}

I know it's long, and maybe complicated to explain, but I hope you can help me. Thanks in advance

dmi
  • 1
  • 1
  • i found a solution, but i don't know if it's a long term correction. i pass group.contracts as initial value to my reducer – dmi Mar 02 '23 at 20:50
  • Welcome to StackOverflow! I didn't read your question very carefully (it's late here), but on the surface it sounds like you run into an issue with pass-by-reference in Javascript. When working with state you need to be careful to make copies of objects as necessary. Or I am completely missing the point, in which case you can ignore me :) https://stackoverflow.com/questions/13104494/does-javascript-pass-by-reference – Mike Szyndel Mar 02 '23 at 22:31
  • Thanks @MikeSzyndel for your answer, i finally found the problem :) it was not a pass-by-ref issue, but i used the index of the array to define the key of the group component... – dmi Mar 04 '23 at 14:13

1 Answers1

0

I finally found the problem: I was using the index of the array to define the key of my group, so each time I deleted an item, the index of the group changed and so it created a new group component

NEVER USE INDEX AS KEY

dmi
  • 1
  • 1