2

I have the following situation, I've made a list that adds (after pressing a button) a React Node. The model final layout is something like this

enter image description here

The current code is the following:

import * as React from 'react';
import { Icon } from '../icon/icon';
import { Button } from '../..';

export interface PriorizationListProps {
    children: React.ReactNode;
    limit?: number;
}

export const PriorizationList: React.FunctionComponent<PriorizationListProps> = ({ limit, children }) => {
    const [items, setItems] = React.useState<React.ReactNode[]>([]);

    const move = (currentIndex: number, futureIndex: number) => {
        if (futureIndex !== -1 && futureIndex < items.length) {
            const item = items[currentIndex];
            const movingItem = items[futureIndex];
            items[currentIndex] = movingItem;
            items[futureIndex] = item;
            setItems(items);
        }
    };

    const deleteItem = (index: number) => {
        setItems(prevItems => prevItems.filter((_,i) => i !== index));
    };

    const addItem = () => {
        const newItems = items.concat([children]);
        setItems(newItems);
    };

    const disabledByLimit = () => limit === items.length;

    return (
        <>
            {items.map((item, index) => {
                return (
                    <div className="PriorizationList">
                        <span className="PriorizationList-order">{`${index + 1}.`}</span>
                        <span className="PriorizationList-content">{item}</span>
                        <span className="PriorizationList-icon">
                            <Icon type="pushUp" onClick={() => move(index, index - 1)} />
                            <Icon type="pushDown" onClick={() => move(index, index + 1)} />
                            <Icon type="delete" onClick={() => deleteItem(index)} />
                        </span>
                    </div>
                );
            })}
            <Button type="text" className="PriorizationList-button" onClick={addItem} disabled={disabledByLimit()}>
                <span>Add</span>
                <Icon type="add" />
            </Button>
        </>
    );
};

PriorizationList.defaultProps = {
    limit: 10
};

export default PriorizationList;

The two issues I'm having are, when I delete an item from the list, it always deletes the last one. And the moving arrows are not working. I tried multiple changes but none of them worked.

1 Answer 1

1

I believe items.concat([children]) is creating new components, therefore you will also need to pass the move() and delete() methods as callbacks (or pass items in as an argument). Additionally, items.concat([children]) is making copy of the old components (including the props in their initial values) and putting them back into state, thus only the last item is getting the final state (in fact its likely one behind, ie your first item rendered is passed an empty this.state.items array as it wont yet include itself as state is set after creation).

Maybe try to either convert to a class based component which will allow you to be explicit with binding this or use a renderItems type method to generate content without creating new components (shown in my snippet below)

Also:

  • Avoid mutating state array with [...items] where appropriate.
  • Dynamically generated content (such as with a loop or map) needs to have a a unique key prop for each item generated. Here I used Math.random() (which may not be suitable for production)
  • You should be checking futureIndex < items.length

I've created a working simplified/reworked example in JSX that may help you get where you need to go

UPDATED Snippet using renderItems

const PriorizationList = () => {
    const [items, setItems] = React.useState([]);
    
    const move = (currentIndex, futureIndex) => {
        if (futureIndex !== -1 && futureIndex < items.length ) {
            const tempItemsAray = [...items]
            const item = tempItemsAray[currentIndex];
            const movingItem = tempItemsAray[futureIndex];
            tempItemsAray[currentIndex] = movingItem;
            tempItemsAray[futureIndex] = item;
            setItems(tempItemsAray);
        }
    };
    const deleteItem = (index) => {
        setItems(prevItems => prevItems.filter((_,i) => i !== index));
    };
    const addItem = () => {
        const id = String(Math.floor(Math.random() * 100000))
        const newItems = [...items , { id }];
        setItems(newItems);
    };

    const renderItems = () => {
        return items.map((item, index) => {
            return (
                <div>
                    <div>
                        <span>Item: {item.id}</span>
                        <button type="pushUp" onClick={() => move(index, index - 1)} >up</button>
                        <button type="pushDown" onClick={() => move(index, index + 1)} >down</button>
                        <button type="delete" onClick={() => deleteItem(index)} >delete</button>
                    </div>
                </div>
            );
        }) 
    }

    return (
        <div>
            <button type="text" onClick={addItem} > Add </button>
            
            {renderItems()}
        </div>
    );
};

ReactDOM.render(
    <PriorizationList />,
    document.getElementById("react")
);
<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>

<div id="react"></div>

Previous Snippet Passing With Callbacks

const PriorizationList = () => {
    const [items, setItems] = React.useState([]);
    
    const move = (currentIndex, futureIndex) => {
        if (futureIndex !== -1 && futureIndex < items.length ) {
            const tempItemsAray = [...items]
            const item = tempItemsAray[currentIndex];
            const movingItem = tempItemsAray[futureIndex];
            tempItemsAray[currentIndex] = movingItem;
            tempItemsAray[futureIndex] = item;
            setItems(tempItemsAray);
        }
    };
    const deleteItem = (index) => {
        setItems(prevItems => prevItems.filter((_,i) => i !== index));
    };
    const addItem = () => {
        const id = String(Math.floor(Math.random() * 100000))
        const newItems = [...items , { id , jsx : LineItemJSX }];
        setItems(newItems);
    };
    
    const LineItemJSX = ({deleteItem , move , index , item}) => {
        return (
            <div>
                <span>Item: {item.id}</span>
                <button type="pushUp" onClick={() => move(index, index - 1)} >up</button>
                <button type="pushDown" onClick={() => move(index, index + 1)} >down</button>
                <button type="delete" onClick={() => deleteItem(index)} >delete</button>
            </div>
        )
    }
    return (
        <div>
            <button type="text" onClick={addItem} > Add </button>

            {items.map((item, index) => {
                const Component = item.jsx
                return (
                    <Component index={index} key={item.id} deleteItem={deleteItem} move={move} item={item}/>
                );
            })}
        </div>
    );
};

ReactDOM.render(
    <PriorizationList />,
    document.getElementById("react")
);
<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>

<div id="react"></div>
Previous Snippet Passing With Items State as Prop

const PriorizationList = () => {
    const [items, setItems] = React.useState([]);
    
    const move = (items , currentIndex, futureIndex) => {
        if (futureIndex !== -1 && futureIndex < items.length ) {
            const tempItemsAray = [...items]
            const item = tempItemsAray[currentIndex];
            const movingItem = tempItemsAray[futureIndex];
            tempItemsAray[currentIndex] = movingItem;
            tempItemsAray[futureIndex] = item;
            setItems(tempItemsAray);
        }
    };
    const deleteItem = (index) => {
        setItems(prevItems => prevItems.filter((_,i) => i !== index));
    };
    const addItem = () => {
        const id = String(Math.floor(Math.random() * 100000))
        const newItems = [...items , { id , jsx : LineItemJSX }];
        setItems(newItems);
    };
    
    const LineItemJSX = ({items , index , item}) => {
        return (
            <div>
                <span>Item: {item.id}</span>
                <button type="pushUp" onClick={() => move(items ,index, index - 1)} >up</button>
                <button type="pushDown" onClick={() => move(items ,index, index + 1)} >down</button>
                <button type="delete" onClick={() => deleteItem(index)} >delete</button>
            </div>
        )
    }
    return (
        <div>
            <button type="text" onClick={addItem} > Add </button>

            {items.map((item, index) => {
                const Component = item.jsx
                return (
                    <Component index={index} key={item.id} item={item} items={items}/>
                );
            })}
        </div>
    );
};

ReactDOM.render(
    <PriorizationList />,
    document.getElementById("react")
);
<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>

<div id="react"></div>

Sign up to request clarification or add additional context in comments.

1 Comment

I've updated my answer with a better approach as well as a better explanation as to whats going on.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.