Using the following useEffect
hook, the updated state is not rendered unless the user touches the screen or even if we place a console.log
under the effect (as indicated in the snippet below):
export const ExerciseForm = ({ definition: { id },}: { definition: ExerciseDefinition;}) => { const initialSet: Set[] = [{ reps: 0, value: 5 }]; const [sets, setSets]: [Set[], Dispatch<Set[]>] = useState(initialSet); // Reset form if definition ID changes useEffect(() => { setSets(initialSet); }, [id]); // Uncommenting this will make it the rerender work?? // console.log('id', id);
Each id
is obviously unique and can confirm that it is updating correctly higher in the component tree from which the prop is passed. I realise this is a common issue, however, none of the similar answers on S/O (or elsewhere on the internets) has yielded a proper solution.
Full components can be found below:
ExerciseForm.tsx
import React, { useState, Dispatch, useEffect } from 'react';import { ExerciseDefinition, Set } from '../constants/Interfaces';import { ScrollView, View } from 'react-native';import { Input, Button } from 'react-native-elements';export const ExerciseForm = ({ definition: { id },}: { definition: ExerciseDefinition;}) => { const initialSet: Set[] = [{ reps: 0, value: 5 }]; const [sets, setSets]: [Set[], Dispatch<Set[]>] = useState(initialSet); // Reset form if definition ID changes useEffect(() => { setSets(initialSet); }, [id]); // Uncommenting this will make it the rerender work?? // console.log('id', id); const updateSet = (index: number, field: 'reps' | 'value', value: string) => { const updatedSets = [...sets]; updatedSets[index][field] = parseInt(value); setSets(updatedSets); }; const addSet = () => { const updatedSets = [...sets, { ...sets[sets.length - 1] }]; setSets(updatedSets); }; return (<ScrollView> {sets.map(({ reps, value }: Set, index: number) => (<View key={index}><Input label="Reps" keyboardType="numeric" value={reps ? reps.toString() : ''} onChange={({ nativeEvent }) => updateSet(index, 'reps', nativeEvent.text) } /><Input label="Weight" keyboardType="numeric" value={value ? value.toString() : ''} onChange={({ nativeEvent }) => updateSet(index, 'value', nativeEvent.text) } /></View> ))}<Button title="Add set" onPress={() => addSet()} /></ScrollView> );};
HomeScreen.tsx
import React, { useContext, useReducer, useEffect, useState, Dispatch,} from 'react';import { StyleSheet, Picker, ScrollView, ActivityIndicator,} from 'react-native';import { Text } from '../components/Themed';import { UserContext } from '../services/context';import { exerciseDefinitionReducer, initialExerciseDefinitionState, exerciseDefinitionActions,} from '../reducers/exerciseDefinition';import { ExerciseDefinition } from '../constants/Interfaces';import { ExerciseForm } from '../components/ExerciseForm';export default function HomeScreen() { const { state: { firstName }, } = useContext(UserContext); const [{ definitions, loading }, definitionDispatch] = useReducer( exerciseDefinitionReducer, initialExerciseDefinitionState ); const [selectedDefintion, setSelectedDefinition]: [ ExerciseDefinition | undefined, Dispatch<ExerciseDefinition> ] = useState(); // Get definitions on mount useEffect(() => { exerciseDefinitionActions(definitionDispatch).getDefinitions(); }, []); // Set default definition to first item useEffect(() => { !selectedDefintion && setSelectedDefinition(definitions[0]); }, [definitions]); return (<ScrollView><Text style={styles.title}>{firstName}</Text> {loading && <ActivityIndicator />} {/* TODO: decide whether to use this R/N Picker or NativeBase picker */}<Picker selectedValue={selectedDefintion?.id} onValueChange={(id) => { const definition = definitions.find((def) => def.id === id); definition && setSelectedDefinition(definition); }}> {definitions.map((defintion) => { const { title, id } = defintion; return <Picker.Item key={id} label={title} value={id} />; })}</Picker> {selectedDefintion && <ExerciseForm definition={selectedDefintion} />}</ScrollView> );}const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 20, fontWeight: 'bold', }, separator: { marginVertical: 30, height: 1, width: '80%', },});
(Edit 29/09/2020)
Interestingly, neither of the following console.logs
are executed either until the user touches the screen (or the noted commented lined is uncommented), suggesting that the issue is certainly to do with the useEffect
statement or its dependency:
// Reset form if definition ID changes useEffect(() => { console.log('old ID', id); setSets(initialSet); console.log('new ID', id); }, [id]); // Uncommenting this will make it the rerender work?? // console.log('id', id);