【问题标题】:How to maintain state of child components when they are filtered through the parent component?子组件通过父组件过滤时如何保持状态?
【发布时间】:2020-07-21 01:29:16
【问题描述】:

我正在使用 create react app 构建一个小应用程序来提高我的反应知识,但现在卡在状态管理上。

应用程序通过父组件上的 JSON 数据进行映射,并打印 6 个“图像卡片”作为子组件,其中包含一组“标签”来描述它以及作为道具传递的其他数据(url、标题等)。

每张卡片都有一个输入,您可以在现有列表中添加更多标签。

在父组件上有一个输入,可用于通过标签过滤卡片。 (仅过滤默认标签而不是添加到卡片的新标签)。

我想要实现的是在每张卡被过滤时保持其状态。目前发生的情况是,如果我向卡片添加新标签并使用多个标签进行过滤,则只有初始过滤的卡片包含新标签,其余的会使用其默认标签重新渲染。谁能告诉我哪里出错了,谢谢。

如果它使事情变得更容易,我的项目也可以被克隆 https://github.com/sai-re/assets_tag

data.json 示例

{
    "assets": [
        {
            "url": "https://images.unsplash.com/photo-1583450119183-66febdb2f409?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=200&ixlib=rb-1.2.1&q=80&w=200",
            "title": "Car",
            "tags": [
                { "id": "USA", "text": "USA" },
                { "id": "Car", "text": "Car" }
            ],
            "suggestions": [
                { "id": "Colour", "text": "Colour" },
                { "id": "Motor", "text": "Motor" },
                { "id": "Engineering", "text": "Engineering" }
            ]
        },
        {
            "url": "https://images.unsplash.com/photo-1582996269871-dad1e4adbbc7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=200&ixlib=rb-1.2.1&q=80&w=200",
            "title": "Plate",
            "tags": [
                { "id": "Art", "text": "Art" },
                { "id": "Wood", "text": "Wood" },
                { "id": "Spoon", "text": "Spoon" }
            ],
            "suggestions": [
                { "id": "Cutlery", "text": "Cutlery" },
                { "id": "Serenity", "text": "Serenity" }
            ]
        }
    ]
}

父组件

import React, {useState} from 'react';
import Item from './Item'
import data from '../../data.json';

import './Assets.scss'

function Assets() {
    const [state, updateMethod] = useState({tag: "", tags: []});

    const printList = () => {
        //if tag in filter has been added        
        if (state.tags.length > 0) {
            return data.assets.map(elem => {
                //extract ids from obj into array
                const dataArr = elem.tags.map(item => item.id);
                const stateArr = state.tags.map(item => item.id);

                //check if tag is found in asset
                const doesTagExist = stateArr.some(item => dataArr.includes(item));
                //if found, return asset 
                if (doesTagExist) return <Item key={elem.title} data={elem} />;
            })
        } else {
            return data.assets.map(elem => (<Item key={elem.title} data={elem} /> ));
        }
    };

    const handleClick = () => {
        const newTag = {id: state.tag, text: state.tag};
        const copy = [...state.tags, newTag];

        if (state.tag !== "") updateMethod({tag: "", tags: copy});
    }

    const handleChange = e => updateMethod({tag: e.target.value, tags: state.tags});

    const handleDelete = i => {
        const copy = [...state.tags];
        let removed = copy.filter((elem, indx) => indx !== i);

        updateMethod({tag: state.tag, tags: removed});
    }

    return (
        <div className="assets">
            <div className="asset__filter">
                <h3>Add tags to filter</h3>
                <ul className="asset__tag-list">
                    {state.tags.map((elem, i) => (
                        <li className="asset__tag" key={`${elem.id}_${i}`} >
                            {elem.text}

                            <button className="asset__tag-del" onClick={() => handleDelete(i)}>x</button>
                        </li>
                    ))}
                </ul>

                <input 
                    type="text" 
                    value={state.tag}
                    onChange={handleChange} 
                    placeholder="Enter new tag" 
                    className="asset__tag-input"
                />

                <button className="asset__btn" onClick={handleClick}>Add</button>
            </div>

            <div className="item__list-holder">
                {printList()}
            </div>
        </div>
    );  
}

export default Assets;

子组件

import React, {useState, useEffect} from 'react';

function Item(props) {
    const [state, updateMethod] = useState({tag: "", tags: []});

    const handleClick = () => {
        //create new tag from state
        const newTag = {id: state.tag, text: state.tag};
        //create copy of state and add new tag
        const copy = [...state.tags, newTag];
        //if state is not empty update state with new tags
        if (state.tag !== "") updateMethod({tag: "", tags: copy});
    }

    const handleChange = e => updateMethod({tag: e.target.value, tags: state.tags});

    const handleDelete = i => {
        //copy state
        const copy = [...state.tags];
        //filter out tag to be deleted
        let removed = copy.filter((elem, indx) => indx !== i);
        //add updated tags to state
        updateMethod({tag: state.tag, tags: removed});
    }

    useEffect(() => {
        console.log("item rendered");
        //when first rendered, add default tags from json to state
        updateMethod({tag: "", tags: props.data.tags});
    }, [props.data.tags]);

    const assets = props.data;

    return (
        <div className="item">
            <img src={assets.url} alt="assets.title"/>
            <h1 className="item__title">{assets.title}</h1>

            <div className="item__tag-holder">
                <ul className="item__tag-list">
                    {state.tags.map((elem, i) => (
                        <li className="item__tag" key={`${elem.id}_${i}`} >
                            {elem.text}
                            <button className="item__tag-del" onClick={() => handleDelete(i)}>x</button>
                        </li>
                    ))}
                </ul>

                <input 
                    type="text" 
                    value={state.tag} 
                    onChange={handleChange} 
                    placeholder="Enter new tag" 
                    className="item__tag-input"
                />

                <button className="item__btn" onClick={handleClick}>Add</button>
            </div>
        </div>
    );
}

export default Item;

【问题讨论】:

  • 那些输入值必须处于父状态。因此,当它们从渲染中过滤出来时,它会被记住。会不会是这个问题?

标签: javascript reactjs react-hooks create-react-app react-state-management


【解决方案1】:

渲染所有项目,即使它们被过滤掉,并且只隐藏使用 CSS 过滤掉的项目 (display: none):

const printList = () => {
    //if tag in filter has been added        
    if (state.tags.length > 0) {
        // create a set of tags in state once
        const tagsSet = new Set(state.tags.map(item => item.id));
        return data.assets.map(elem => {
            //hide if no tag is found
            const hideElem = !elem.tags.some(item => tagsSet.has(item.id));

            //if found, return asset 
            return <Item key={elem.title} data={elem} hide={hideElem} />;
        })
    } else {
        return data.assets.map(elem => (<Item key={elem.title} data={elem} /> ));
    }
};

在项目本身中,使用 hide 属性使用 style 属性或 css 类通过 CSS 隐藏项目:

return (
    <div className="item" style={{ display: props.hide ? 'none' : 'block' }}>

您还可以通过始终创建 Set 来进一步简化 printList(),即使 state.tags 为空,如果为空,hideElem 将是 false

const printList = () => {
  const tagsSet = new Set(state.tags.map(item => item.id));

  return data.assets.map(elem => {
    //hide if state.tags is empty or no selected tags
    const hideElem = tagsSet.size > 0 && !elem.tags.some(item => tagsSet.has(item.id));

    //if found, return asset 
    return (
      <Item key={elem.title} data={elem} hide={hideElem} />
    );
  })
};

【讨论】:

    【解决方案2】:

    您面临的问题是消失的卡牌被卸载,这意味着它们的状态丢失了。最好的解决方案是将添加到卡片的新自定义标签保留在父组件中,因此无论卡片是否已安装,它都是持久的。以下是修改后的文件:

    父组件

    import React, {useState} from 'react';
    import Item from './Item'
    import data from '../../data.json';
    
    import './Assets.scss'
    
    function Assets() {
        const [state, updateMethod] = useState({tag: "", tags: []});
    
        const [childrenTags, setChildrenTags] = useState(data.assets.map(elem => elem.tags));
    
        const addChildrenTag = (index) => (tag) => {
            let newTags = Array.from(childrenTags)
            newTags[index] = [...newTags[index], tag]
    
            setChildrenTags(newTags)
        }
    
        const removeChildrenTag = (index) => (i) => {
            let newTags = Array.from(childrenTags)
            newTags[index] = newTags[index].filter((elem, indx) => indx !== i)
    
            setChildrenTags(newTags)
        }
    
        const printList = () => {
            //if tag in filter has been added        
            if (state.tags.length > 0) {
                return data.assets.map((elem, index) => {
                    //extract ids from obj into array
                    const dataArr = elem.tags.map(item => item.id);
                    const stateArr = state.tags.map(item => item.id);
    
                    //check if tag is found in asset
                    const doesTagExist = stateArr.some(item => dataArr.includes(item));
                    //if found, return asset 
                    if (doesTagExist) 
                        return (
                            <Item 
                                key={elem.title} 
                                data={elem} 
                                customTags={childrenTags[index]} 
                                addCustomTag={addChildrenTag(index)}
                                removeCustomTag={removeChildrenTag(index)}
                            />
                        )
                })
            } else {
                return data.assets.map((elem, index) => (
                    <Item 
                        key={elem.title} 
                        data={elem} 
                        customTags={childrenTags[index]} 
                        addCustomTag={addChildrenTag(index)}
                        removeCustomTag={removeChildrenTag(index)}
                    />
                ));
            }
        };
    
        const handleClick = () => {
            const newTag = {id: state.tag, text: state.tag};
            const copy = [...state.tags, newTag];
    
            if (state.tag !== "") updateMethod({tag: "", tags: copy});
        }
    
        const handleChange = e => updateMethod({tag: e.target.value, tags: state.tags});
    
        const handleDelete = i => {
            const copy = [...state.tags];
            let removed = copy.filter((elem, indx) => indx !== i);
    
            updateMethod({tag: state.tag, tags: removed});
        }
    
        return (
            <div className="assets">
                <div className="asset__filter">
                    <h3>Add tags to filter</h3>
                    <ul className="asset__tag-list">
                        {state.tags.map((elem, i) => (
                            <li className="asset__tag" key={`${elem.id}_${i}`} >
                                {elem.text}
    
                                <button className="asset__tag-del" onClick={() => handleDelete(i)}>x</button>
                            </li>
                        ))}
                    </ul>
    
                    <input 
                        type="text" 
                        value={state.tag}
                        onChange={handleChange} 
                        placeholder="Enter new tag" 
                        className="asset__tag-input"
                    />
    
                    <button className="asset__btn" onClick={handleClick}>Add</button>
                </div>
    
                <div className="item__list-holder">
                    {printList()}
                </div>
            </div>
        );  
    }
    
    export default Assets;
    

    子组件

    import React, {useState, useEffect} from 'react';
    
    function Item(props) {
        const [state, updateMethod] = useState({tag: ""});
        cosnst tags = props.customTags
        cosnst addCustomTag = props.addCustomTag
        cosnst removeCustomTag = props.removeCustomTag
    
        const handleClick = () => {
            if (state.tag !== "") addCustomTag(state.tag);
        }
    
        const handleChange = e => updateMethod({tag: e.target.value});
    
        const handleDelete = i => {
            removeCustomTag(i);
        }
    
        const assets = props.data;
    
        return (
            <div className="item">
                <img src={assets.url} alt="assets.title"/>
                <h1 className="item__title">{assets.title}</h1>
    
                <div className="item__tag-holder">
                    <ul className="item__tag-list">
                        {tags.map((elem, i) => (
                            <li className="item__tag" key={`${elem.id}_${i}`} >
                                {elem.text}
                                <button className="item__tag-del" onClick={() => handleDelete(i)}>x</button>
                            </li>
                        ))}
                    </ul>
    
                    <input 
                        type="text" 
                        value={state.tag} 
                        onChange={handleChange} 
                        placeholder="Enter new tag" 
                        className="item__tag-input"
                    />
    
                    <button className="item__btn" onClick={handleClick}>Add</button>
                </div>
            </div>
        );
    }
    
    export default Item;
    

    希望这会有所帮助,如果有什么不清楚的地方,我可以添加一些 cmets :)

    【讨论】:

    • 嗨,亚当,我尝试将其集成到其中,但是当我向卡片添加新标签时,会添加一个空字符串而不是输入。尽管您的解决方案在过滤后确实保持相同的状态,但我是否正确地说您基本上将卡片状态保留在父级并使用回调通过道具更新它?
    • 这是正确的,父级保留所有状态,回调addCustomTagremoveCustomTag用于修改此状态
    • 我真的不确定,为什么将一个空字符串推送到标签而不是您的输入。也许尝试将state.tag 记录在handleClicktag 内部addChildrenTag 以检查所有内容是否正确传递?
    猜你喜欢
    • 2020-01-13
    • 2021-04-04
    • 2021-01-07
    • 2019-06-07
    • 1970-01-01
    • 2020-04-10
    • 2021-05-22
    • 2019-12-12
    • 2019-01-08
    相关资源
    最近更新 更多