简体   繁体   中英

Nested lists - How to modify (delete, add, update) items from a nested list using item's buttons and useState?

My first idea was to try to delete items from a nested list. I've started using SectionList and a component that has an useState that manages the data changes of the SectionList. I'm trying to solve it using SectionList but it'd be great to display all possible alternatives (FlatList, ViewList, etc).

I figured out how to delete a whole section list (and his items) but not one item by one. I've read plenty of posts, but I did find nothing related to managing SectionList items. Maybe there's an answer out there for FlatList nested items.

Here I left an example code ready to use (without styles) based on React Native official docs:

import React, { useEffect } from "react";
import { StyleSheet, Text, View, SafeAreaView, SectionList, StatusBar, Button } from "react-native";
import { useState } from "react";


const dataResource = [
  {
    title: "Main dishes",
    data: ["Pizza", "Burger", "Risotto"],
    n: "delete",
    k: 1
  },
  {
    title: "Sides",
    data: ["French Fries", "Onion Rings", "Fried Shrimps"],
    n: "delete",
    k: 2
  },
];



function App() {
  const [state, setState] = useState(dataResource)


  const Item = ({ dish, obj, i}) => (
    <View >

      <Text >{dish} </Text>
          <Button title={obj.n} onPress={() => {}} />     // add the handler
    </View>
  );

  const SectionComponent = ({ t, k }) => (
    <View>
      <Text >{t} </Text>
      <Button title={'section'} onPress={() => { setState(state.filter(e => e.k != k)) }} />
    </View>
  );

  return (
    <SafeAreaView >
      <SectionList
        sections={state}
        keyExtractor={(item, index) => item + index}
        renderItem={({ item, section, index}) => <Item dish={item} obj={section} i={index}/>}
        renderSectionHeader={({ section: { title, k } }) => <SectionComponent k={k} t={title} />}
      />
    </SafeAreaView>
  );
}



export default App;


I think how you display the data is trivial. The component you use will just change how you access the data, not how you update it. What you need is helper functions for editing the data. With those in place you can do things like add/remove section items, and editing section items themselves:

import React, { useState } from 'react';
import {
  Text,
  View,
  StyleSheet,
  SafeAreaView,
  SectionList,
  Button,
  TextInput,
} from 'react-native';

const dataResource = [
  {
    title: 'Main dishes',
    data: ['Pizza', 'Burger', 'Risotto'],
    id: 1,
  },
  {
    title: 'Sides',
    data: ['French Fries', 'Onion Rings', 'Fried Shrimps'],
    id: 2,
  },
];
export default function App() {
  const [state, setState] = useState(dataResource);
  const [sectionTitle,setSectionTitle] = useState('Drinks')
  
  const editItem = (itemId, newValue) => {
    let newState = [...state];
    let itemIndex = newState.find((item) => item.id == itemId);
    if (itemIndex < 0) return;
    newState[itemIndex] = {
      ...newState[itemIndex],
      ...newValue,
    };
    setState(newState);
  };
  const addSectionItem = (title)=>{
    let newState = [...state]
    newState.push({
      title,
      data:[],
      id:newState.length+1
    })
    setState(newState)
  }

  const removeFood = (itemId, food) => {
    let currentItem = state.find((item) => item.id == itemId);
    console.log(currentItem);
    currentItem.data = currentItem.data.filter((item) => item != food);
    console.log(currentItem.data);
    editItem(itemId, currentItem);
  };
  const addFood = (itemId, food) => {
    let currentItem = state.find((item) => item.id == itemId);
    console.log(currentItem.data);
    currentItem.data.push(food);
    console.log(currentItem.data);
    editItem(itemId, currentItem);
  };

  const Item = ({ item, section, index }) => {
    return (
      <View style={styles.row}>
        <Text>{item} </Text>
        <Button
          title={'Delete'}
          onPress={() => {
            removeFood(section.id, item);
          }}
        />
      </View>
    );
  };

  const SectionHeader = ({ title, id }) => {
    return (
      <View style={styles.header}>
        <Text style={{ fontSize: 18 }}>{title} </Text>
        <Button
          title={'X'}
          onPress={() => {
            setState(state.filter((e) => e.id != id));
          }}
        />
      </View>
    );
  };
  const SectionFooter = ({ id }) => {
    const [text, setText] = useState('');
    return (
      <View style={[styles.row, styles.inputWrapper]}>
        <Text>Add Entry</Text>
        <TextInput
          value={text}
          onChangeText={setText}
          style={{ borderBottomWidth: 1 }}
        />
        <Button title="Add" onPress={() => addFood(id, text)} />
      </View>
    );
  };
  return (
    <SafeAreaView>
      <Button title="Reset list" onPress={() => setState(dataResource)} />
      <View style={[styles.row, styles.inputWrapper]}>
        <Text>Add New Section</Text>
        <TextInput
          value={sectionTitle}
          onChangeText={setSectionTitle}
          style={{ borderBottomWidth: 1 }}
        />
        <Button title="Add" onPress={() => addSectionItem(sectionTitle)} />
      </View>
      <View style={styles.sectionListWrapper}>
        <SectionList
          sections={state}
          keyExtractor={(item, index) => item + index}
          renderItem={Item}
          renderSectionHeader={({ section }) => <SectionHeader {...section} />}
          renderSectionFooter={({ section }) => <SectionFooter {...section} />}
        />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',
    width: '100%',
    justifyContent: 'space-between',
    padding: 5,
    alignItems: 'center',
  },
  header: {
    flexDirection: 'row',
    paddingVertical: 5,
    width: '100%',
    borderBottomWidth: 1,
  },
  inputWrapper: {
    paddingVertical: 15,
    marginBottom: 10,
  },
  sectionListWrapper: {
    padding: 5,
  },
});

Here's a demo

Hope I'm following SOF rules: I wanted to comment @PhantomSpooks' answer with a code block and it's an alternative way to write his solution.

By passing the whole section object and creating a deep copy of the state. I don't know if I'm missing something but this works as well.

renderSectionFooter={({ section }) => <SectionFooter {...section} section={section} />}
    

const addFood = (itemId, food, section) => {
    section.data.push(food);
    var deepStateCopy = JSON.parse(JSON.stringify(state));
    setState(deepStateCopy);
};

I've improved my answer to give a clearer example of how to manipulate SectionList. An example of how to add/delete/modify sections and its data is provided .

import React from 'react';

export default function useStateHelpers(state, setState) {
  const editSection = (sectionId, newValue) => {
    // parse and stringify is used to clone properly
    let newState = JSON.parse(JSON.stringify(state));
    let itemIndex = newState.findIndex((item) => item.id == sectionId);
    if (itemIndex < 0) return;
    const section = {
      ...newState[itemIndex],
      ...newValue,
    };
    newState[itemIndex] = section;
    setState(newState);
  };
  const editSectionDataItem = (sectionId, itemId, newValue) => {
    const newState = JSON.parse(JSON.stringify(state));
    const sectionIndex = newState.findIndex(
      (section) => section.id == sectionId
    );
    const section = newState[sectionIndex];
    const itemIndex = section.data.findIndex((item) => item.id == itemId);
    let item = section.data[itemIndex];
    section.data[itemIndex] = {
      ...item,
      ...newValue,
    };

    editSection(sectionId, section);
  };
  const editSectionTitle = (sectionId, title) =>
    editSection(sectionId, { title });

  const setSelectSectionItem = (sectionId, itemId, isSelected) => {
    editSectionDataItem(sectionId, itemId, { isSelected });
  };

  const removeSectionDataItem = (sectionId, food) => {
    let newState = JSON.parse(JSON.stringify(state));
    let sectionIndex = newState.findIndex((section) => section.id == sectionId);
    let section = newState[sectionIndex];
    section.data = section.data.filter((item) => item.title != food);
    editSection(sectionId, section);
  };

  const addSectionDataItem = (sectionId, title, price) => {
    let newState = JSON.parse(JSON.stringify(state));
    let sectionIndex = newState.findIndex((section) => section.id == sectionId);
    let section = newState[sectionIndex];
    section.data.push({
      title,
      price,
      id: `section-${sectionId}-${section.data.length + 1}`,
    });
    editSection(sectionId, section);
  };

  const addSection = (title, data = [], id) => {
    let newState = JSON.parse(JSON.stringify(state));
    newState.push({
      title,
      data,
      id: newState.length + 1,
    });
    setState(newState);
  };
  
  const helpers = {
    editSection,
    editSectionTitle,
    removeSectionDataItem,
    addSectionDataItem,
    addSection,
    setSelectSectionItem,
    editSectionDataItem,
  };
  return helpers;
}

import React, { useState } from 'react';
import {
  StyleSheet,
  SafeAreaView,
  View,
  Button,
  SectionList,
} from 'react-native';

import data from './data';
import StateContext from './StateContext';
import useStateHelpers from './hooks/useStateHelpers';

import Header from './components/SectionHeader';
import Footer from './components/SectionFooter';
import Item from './components/SectionItem';
import TextInput from './components/Input';

export default function App() {
  const [state, setState] = useState(data);
  const [sectionTitle, setSectionTitle] = useState('');
  const helpers = useStateHelpers(state, setState);

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <StateContext.Provider value={{ state, setState, ...helpers }}>
        <View style={styles.container}>
          <Button title="Reset list" onPress={()=>setState(data)} />
          <TextInput
            label={'Add New Section'}
            value={sectionTitle}
            onChangeText={setSectionTitle}
            onRightIconPress={() => {
              helpers.addSection(sectionTitle);
              setSectionTitle('');
            }}
          />
          <View style={styles.sectionListWrapper}>
            <SectionList
              style={{ flex: 1 }}
              sections={state}
              renderItem={(props) => <Item {...props} />}
              renderSectionHeader={(props) => <Header {...props} />}
              renderSectionFooter={(props) => <Footer {...props} />}
            />
          </View>
        </View>
      </StateContext.Provider>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 8,
  },
  sectionListWrapper: {
    padding: 5,
    flex: 1,
  },
});
import React, { useContext } from 'react';
import { View, TouchableOpacity, StyleSheet, Text, Button } from 'react-native';
import StateContext from '../StateContext';

export default function Item({ item, section, index }) {
  const { 
    removeSectionDataItem,
    setSelectSectionItem
    } = useContext(StateContext);
  const hasPrice = item.price.toString().trim().length  > 0;
  return (
    <TouchableOpacity
      style={[styles.container, item.isSelected && styles.selectedContainer]}
      onPress={() =>
        setSelectSectionItem(section.id, item.id, !item.isSelected)
      }
    >
      <View style={styles.row}>
        <Text>
          {item.title + (hasPrice ? ' | ' : '')}
          {hasPrice && <Text style={{ fontWeight: '300' }}>{item.price}</Text>}
        </Text>
        <Button
          title={'Delete'}
          onPress={() => {
            removeSectionDataItem(section.id, item.title);
          }}
        />
      </View>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  container: {
    marginVertical: 2,
  },
  selectedContainer: {
    backgroundColor: 'rgba(0,0,0,0.2)',
  },
  row: {
    flexDirection: 'row',
    width: '100%',
    justifyContent: 'space-between',
    padding: 5,
    alignItems: 'center',
  },
});

/*SectionFooter.js*/
import React, { useState, useContext, useEffect } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import TextInput from './Input';
import StateContext from '../StateContext';

export default function SectionFooter({ section }) {
  const [title, setTitle] = useState('');
  const [price, setPrice] = useState('');
  const [titleWasEdited, setTitleWasEdited] = useState(false);
  const { addSectionDataItem, editSectionDataItem } = useContext(StateContext);
  const selectedItem = section.data.find((item) => item.isSelected);
  // listen for item selection
  useEffect(() => {
    // pass item values to text input
    setTitle(selectedItem?.title || '');
    setPrice(selectedItem?.price || '');
    // reset title/price input
    setTitleWasEdited(false);
  }, [selectedItem]);
  return (
    <View style={styles.container}>
      <Text>
        {selectedItem 
          ? 'Editing ' + selectedItem.title 
          : 'Add New Entry'
        }
      </Text>
      {/*one input handles both title and price*/}
      <TextInput
        label={titleWasEdited ? 'Price' : 'Title'}
        value={titleWasEdited ? price : title}
        onChangeText={titleWasEdited ? setPrice : setTitle}
        rightIconName={titleWasEdited ? 'plus' : 'arrow-right'}
        // rightIcon is disabled when theres no text
        // but price is allowed to be empty
        allowEmptySubmissions={titleWasEdited}
        style={{ width: '80%' }}
        onRightIconPress={() => {
          // after title is edited allow submission
          if (titleWasEdited) {
            // if item was edited update it
            if (selectedItem) {
              editSectionDataItem(section.id, selectedItem.id, {
                title,
                price,
                isSelected: false,
              });
            }
            // or add new item
            else {
              addSectionDataItem(section.id, title, price);
            }
            setTitle('');
            setPrice('');
          }
          // toggle between title & price
          setTitleWasEdited((prev) => !prev);
        }}
      />
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    marginVertical: 20,
    alignItems: 'center',
  },
});
import React, { useContext } from 'react';
import { View, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-paper';
import StateContext from '../StateContext';

export default function SectionHeader({ section: { id, title } }) {
  const { editTitle, setState, state } = useContext(StateContext);
  return (
    <View style={{ borderBottomWidth: 1 }}>
      <View style={styles.header}>
        <TextInput
          style={styles.titleInput}
          value={title}
          onChangeText={(text) => {
            editTitle(id, text);
          }}
          right={
            <TextInput.Icon
              name="close"
              onPress={() => {
                setState(state.filter((sec) => sec.id != id));
              }}
            />
          }
          underlineColor="transparent"
          activeUnderlineColor="transparent"
          outlineColor="transparent"
          activeOutlineColor="transparent"
          selectionColor="transparent"
          dense
        />
      </View>
    </View>
  );
}
const styles = StyleSheet.create({
  header: {
    flexDirection: 'row',
    width: '100%',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  titleInput: {
    fontSize: 18,
    backgroundColor: 'transparent',
    borderWidth: 0,
    borderColor: 'transparent',
    height: 36,
  },
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM