1

Newbie here, I have a page where a user adds and removes input forms by pressing an add/remove button. Each <InputCard/> item has a few <TextInput/> fields. I need changes to these <InputCard/> items to change the state in the parent component. When the <TextInput/> values change in each/any of the <InputCard/> items, I need this change reflected in the parent formState. Code is below:

InputScreen.js

import React, { useState } from 'react';
import { View, StyleSheet, ScrollView, FlatList } from 'react-native';
import { Button, Caption } from 'react-native-paper';
import InputCard from '../components/InputCard';

const InputScreen = props => {
    const [inputList, setInputList] = useState([]);
    const [formState, setFormState] = useState([]);
    const addInput = () => {
        setInputList(inputList.concat(<InputCard key={inputList.length} />));
        setFormState([
            ...formState,
            {
                id: inputList.length,
                substance: "",
                amount: "",
                measure: ""
            },
        ]);
    };

    const handleInputChange = () => {
        // each card has 3 input fields and needs to update the corresponding values in formState
        // where the InputCard key and formState id are corresponding
    };
    
    const removeLastInput = () => {
        if (inputList.length > 0) {
            const lastindex = inputList.length - 1;
            setFormState(formState.filter((item, index) => index !== lastindex));
            setInputList(inputList.filter((item, index) => index !== lastindex));       
        }
    };

    return (
        <ScrollView>
            <View style={styles.col}>
                <View style={styles.row}>
                    <Caption>What substances are you using?</Caption>
                </View>
                <View style={styles.row}>
                    <View>
                        {inputList}
                    </View>
                </View>
                <View>
                    <View style={styles.col}>
                        <Button title='Add' onPress={addInput}>Add</Button>
                    </View>
                    <View style={styles.col}>
                        <Button title='Remove' onPress={removeLastInput}>Remove</Button>
                    </View>
                </View>
            </View>
        </ScrollView>
    )
};

const styles = StyleSheet.create({
    container: {
      backgroundColor: '#c1f5f5',
      paddingVertical: 20,
      paddingHorizontal: 20
    },
    col: {
        flexDirection: 'column',
        paddingVertical: 10,
      },
    row: {
        flexDirection: 'row',
    },
    rowright: {
        flexDirection: 'row',
        justifyContent: 'flex-end'
    },
    half : {
        width: '50%',
        paddingRight: 5,
    },
    quarter : {
        width: '25%',
        paddingHorizontal: 5,
    },
    quarterlast : {
        width: '25%',
        paddingLeft: 5,
    },
    third : {
        width: '33%',
        paddingHorizontal: 5,
    },
    thirdlast : {
        width: '33%',
        paddingLeft: 5,
    },
    substanceconfig : {
        backgroundColor: '#E1F7F7',
        fontSize: 12,
    },
    textfield : {
        borderColor: '#1e5c64',
        borderWidth: 2,
    },
    dropdown: {
        height: 60,
        backgroundColor: '#ffffff',
        borderColor: '#1e5c64',
        borderRadius: 7,
        borderWidth: 1,
        padding: 14
    },
    iconbutton: {
        backgroundColor: '#1e5c64',
        color: '#ffffff',
    },
    slider: {
        flex: 1,
        height: 60
    },
    flex1: {
        flex: 1
    }
});

export default InputScreen;

InputCard.js

import React from "react";
import { View, StyleSheet } from 'react-native';
import { Caption, Card, TextInput } from "react-native-paper";

const InputCard = (props, { formState, handleChange }) => {

    const key = props.key;    

    return (
        <View>
            <Card>
                <Card.Content>
                    <Caption>Item {key}</Caption>
                    <View style={styles.row}>
                        <View style={styles.half}>
                            <TextInput
                                label="substance"
                                value={formState[key].substance}
                                onChangeText={handleChange} // change needs to be made from here to parent
                                mode="outlined"
                                right={<TextInput.Icon name="pill" />}
                                style={styles.textfield}
                            />
                        </View>
                        <View style={styles.quarter}>
                            <TextInput
                                label="amount"
                                value={formState[key].amount}
                                onChangeText={handleChange}
                                mode="outlined"
                                keyboardType="number-pad"
                            />
                        </View>
                        <View style={styles.quarterlast}>
                            <TextInput
                                label="measure"
                                value={formState[key].measure}
                                onChangeText={handleChange}
                                mode="outlined"
                            />
                        </View>
                    </View>
                </Card.Content>
            </Card>
        </View>
    );
}

export default InputCard;

const styles = StyleSheet.create({
    container: {
      backgroundColor: '#c1f5f5',
      paddingVertical: 20,
      paddingHorizontal: 20
    },
    col: {
        flexDirection: 'column',
        paddingVertical: 10,
      },
    row: {
        flexDirection: 'row',
    },
    rowright: {
        flexDirection: 'row',
        justifyContent: 'flex-end'
    },
    half : {
        width: '50%',
        paddingRight: 5,
    },
    quarter : {
        width: '25%',
        paddingHorizontal: 5,
    },
    quarterlast : {
        width: '25%',
        paddingLeft: 5,
    },
    third : {
        width: '33%',
        paddingHorizontal: 5,
    },
    thirdlast : {
        width: '33%',
        paddingLeft: 5,
    },
    substanceconfig : {
        backgroundColor: '#E1F7F7',
        fontSize: 12,
    },
    textfield : {
        borderColor: '#1e5c64',
        borderWidth: 2,
    },
    dropdown: {
        height: 60,
        backgroundColor: '#ffffff',
        borderColor: '#1e5c64',
        borderRadius: 7,
        borderWidth: 1,
        padding: 14
    },
    iconbutton: {
        backgroundColor: '#1e5c64',
        color: '#ffffff',
    },
    slider: {
        flex: 1,
        height: 60
    },
    flex1: {
        flex: 1
    }
});

2
  • What's the problem/question you have about the current code? How is it not working, specifically?
    – ggorlen
    Commented Jan 14, 2022 at 18:31
  • I just don't know how I should implement handleInputChange in the child component, so right now the onChangeText in the InputCard is doing nothing.
    – patataskr
    Commented Jan 14, 2022 at 18:36

1 Answer 1

0

Firstly, change your <InputCard> render like this:

   const addInput = () => {
let newFormState = [
            ...formState,
            {
                id: inputList.length,
                substance: "",
                amount: "",
                measure: ""
            },
        ];
setFormState(newFormState);
setInputList(inputList.concat(<InputCard key={inputList.length} id={inputList.length} formState={newFormState} handleChange={handleInputChange}/>));
        
    };

You should be passing in the props to the child component. You cannot access key property. It is used by react under the hood. Hence use something else, like id. Also you should be passing in the new state value into the <InputCard>. With your current code the first inputCard will not have any data. So instead create the new state value first and then use it.

You also have to set your array state. Since it is an array of objects you need to make a copy and then actually replace the object.

 const handleInputChange = (index, newStateForIndex) => {
       let newFormState = [...formState];
        setFormState([...newStateForIndex.slice(0, index),
     newStateForIndex,
      ...newStateForIndex.slice(index + 1)]);
    };

Now you have to update your call from child component to match the parent components function signature:


onChangeText={(event) => {
handleChange(id,{
...formState,
substance : event.target.value //This key will change based on the input field
});
}
}

Don't forget to use id prop inside your child element, instead of key.

NOTE: You are using index as key which is not recommended. Try to use a unique id, if you do not have one use a library like uuid. Adding to this, your initial code showed that you plan to pass your whole state in your individual inputs. I built the solution on that. But why not pass only the relevant part. Each InputCard should only have its own state's input.

<InputCard key={inputList.length} id={inputList.length} individualFormState={formState[inputList.length]} handleChange={handleInputChange}/>

and setting state would be even easier.

1
  • So why are we declaring newFormState under the handleInputChange function? Also, I'm getting cannot read undefined under the value prop of TextInput, am I reading in formState properly? Like so: const InputCard = (props, { id, formState, handleChange }) => {... ? And value={formState[id].substance} is how I'm reading in value.
    – patataskr
    Commented Jan 14, 2022 at 19:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.