So I have a problem, I've been stuck on for a couple of hours. My state doesn't get updated inside a function. As you can see in my example I have a useState hook which is responsible for keeping the value of the text input. Let's say I type in 'abcd', if i console log the state in the handleChange and outside of it just before return, the state shows correctly, however on the handleHeaderRightButtonPress which is responsible for the saving functionality basically, it doesn't update, it's always my default value, in this case randomVal. Any ideeas why this behaviour could happen or how could i troubleshoot it? Thanks in advance:
My example(I stripped out unnecessary code so it's easier)
const TextAreaScreen = ({ navigation, route }) => {
const placeholder = route.params?.placeholder;
const [value, setValue] = useState('randomval');
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerRight: () =>
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
});
}, []);
const handleChange = (value: string) => {
console.log('here', value); //the updated value shows up correctly
setValue(value);
};
const handleHeaderRightButtonPress = () => {
const onFinish = route.params?.onFinish;
console.log('value in handleFunc', value); // the updated values does NOT work here
onFinish(value);
navigation.goBack();
};
console.log('state val::', value); // updated value shows up correctly
return (
<TextArea
value={value}
placeholder={placeholder}
onChangeText={handleChange}
/>
);
};
export default TextAreaScreen;
Pass value to useEffect like :
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerRight: () =>
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
});
}, [value]);
Just noticed I wasn't updating my useEffect with the value. Fixed by adding it as a dependency in the array:
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerLeft: () => {
const onBackPress = () => {
navigation.goBack();
};
return Platform.select({
ios: (
<NavigationHeader.TextButton
label={t('general.cancel')}
onPress={onBackPress}
/>
),
android: (
<NavigationHeader.IconButton
iconName="times"
label={t('general.cancel')}
onPress={onBackPress}
/>
)
});
},
headerRight: () =>
Platform.select({
ios: (
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
),
android: (
<NavigationHeader.IconButton
iconName="check"
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
)
})
});
}, [value]); // here
Related
I've a jest test that is failing on addition of a new component to the page. The test is about showing of an error alert once error occurs. Code works in local environment but fails during commit.
Error Text:
TestingLibraryElementError: Unable to find an element with the text:
Student is unable to perform register/unregister activities.. This could be because
the text is broken up by multiple elements. In this case, you can
provide a function for your text matcher to make your matcher more
flexible.
Test:
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useMutation: jest.fn((_key, cb) => {
cb();
return { data: null };
})
}));
const useMutation = useMutationHook as ReturnType<typeof jest.fn>;
describe('StatusAlert', () => {
beforeEach(() => {
useMutation.mockReturnValue({});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should show error', () => {
useMutation.mockReturnValueOnce({
isError: true
});
const { getByText } = render(
<StudentRegister
students={[studentStub, studentStub]}
onSuccess={jest.fn()}
/>
);
expect(getByText(ErrorDict.ErrorRequest)).toBeInTheDocument();
});
StudentRegister:
Adding this component is causing the above mentioned error:
interface Props {
selectedStudents: Array<Student>;
onSuccessCallback: () => void;
}
export const StudentSelectionBar: FC<Props> = ({
selectedStudents,
onSuccessCallback
}) => {
const [isOpenDropCourseModal, setisOpenDropCourseModal] =
useState(false);
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) =>
student.id));
}, [selectedStudents]);
const onToggleOpenDropCourseModal = useCallback(() => {
setisOpenDropCourseModal(
(state) => !state
);
}, []);
const {
isError: isDropCourseError,
isSuccess: isDropCourseSuccess,
isLoading: isDropCourseLoading,
mutateAsync: DropCourseMutation,
error: DropCourseError
} = useMutation<void, ApiError>(
() => dropCourse(selectedStudents.map((student) =>
student.id)),
{
onSuccess() {
onToggleOpenDropCourseModal();
onSuccess();
}
}
);
return (
<>
<StatusAlert
isError={isDropCourseError}
isSuccess={isDropCourseSuccess}
errorMessage={
dropCourseError?.errorMessage ||
ErrorMessages.FailedPostRequest
}
successMessage="Students successfully dropped from
course"
/>
<StatusAlert
isError={registerMutation.isError}
isSuccess={registerMutation.isSuccess}
errorMessage={
registerMutation.error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully registered"
/>
<StatusAlert
isError={isError}
isSuccess={isSuccess}
errorMessage={
error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully unregistered"
/>
<Permissions scope={[DropCourseUsers]}>
<LoadingButton
color="error"
variant="contained"
onClick={onToggleDropCourseUserModal}
className={styles['action-button']}
loading={isDropCourseLoading}
loadingPosition="center"
disabled={registerMutation.isLoading || isLoading}
>
drop Course
</LoadingButton>
</Permissions>
<DropCourseModal
isOpen={isOpenDropCourseModal}
onCloseModal={onToggleOpenDropCourseModal}
onArchiveUsers={DropCourseMutation}
users={studentIds}
/>
</>
);
};
Update:
I've noticed that removing useEffect() hook from the component, makes it render correctly in the test. Its function is to update the state variable holding studentIds on every selection on the list.
Is there a way to mock following useEffect hook with dependency in the test?
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) => student.id));
}, [selectedStudents]);
Why doesn't the local storage change happen on first click?
const Menu = () => {
const { auth, setAuth } = useContext(AuthContext);
useEffect(() => {
localStorage.setItem("theme", auth.theme);
}, [auth.theme]);
return (
<header>
<ToggleSwitch
defaultChecked={auth.theme}
onChange={() => {
setAuth((prevState) => ({
...prevState,
theme: !prevState.theme,
}));
}}
/>
</header>
);
};
I have seen the same question asked for setState and the answer seems to be useEffect and ...prevState which I've done.
Edit: It in fact updates from true to false first time but not the other way around.
I am using TreePicker & CheckTreePicker from rsuite package.
I would like to clear the selections programmatically for the tree picker when certain props value is changed. I am able to trigger event in the useEffect when value of selectItemchanges , and I would like to clear all the current selections for treePicker just after it.
const Categories = ({ selectItem }) => {
useEffect(() => {
// INCLUDE LOGIC HERE TO RESET ALL THE FILTERS WHEN the value of selectItem change
}, []);
const handleCategFilters = (value) => {
console.log("do something here with value", value);
};
return (
<CheckTreePicker
data={pickerDT}
onChange={(i) => {
handleCategFilters(i);
}}
/>
);
};
I appreciate yur help. Thank you.
You can manually control the value
const Categories = ({ selectItem }) => {
const [value, setValue] = React.useState([]);
useEffect(() => {
// INCLUDE LOGIC HERE TO RESET ALL THE FILTERS WHEN the value of selectItem change
setValue([]);
}, []);
const handleCategFilters = (value) => {
console.log("do something here with value", value);
};
return (
<CheckTreePicker
data={pickerDT}
value={value}
onChange={(i) => {
handleCategFilters(i);
}}
/>
);
};
I have a react-native-paper TextInput that I want to focus automatically when I navigate to a screen (using react-native-navigation). I have tried setting autoFocus={true} on the TextInput, but that didn't work.
In another attempt, I tried to focus it manually by listening to the 'focus' event on the screen, but that only focused it the first time I opened the screen. Is there any way of getting it to work reliably?
export default function NewAccountScreen({ navigation }) {
const [name, setName] = useState('');
const textInputRef = createRef();
// This isn't working, neither is autoFocus...
const focusOnInput = () => {
textInputRef.current?.focus();
}
navigation.addListener("focus", focusOnInput);
return (
<View>
<TextInput ref={textInputRef} label="Account name" value={name} onChangeText={setName}/>
</View>
)
}
use React.useRef() instead of createRef();
use React.useEffect to listen when ref is defined to can use it.
export default function NewAccountScreen({ navigation }) {
const [name, setName] = useState('');
const textInputRef = React.useRef();
React.useEffect(() => {
if(textInputRef.current){
const unsubscribe = navigation.addListener('focus', () => {
textInputRef.current?.focus()
});
return unsubscribe;
}
}, [navigation, textInputRef.current]);
return (
<View>
<TextInput ref={textInputRef} label="Account name" value={name} onChangeText={setName}/>
</View>
)
}
Update: as #pta2002 comment
I tried this, and it focuses sometimes now, but sometimes it seems to focus and then immediatelly unfocus...
i test the snack, and as he said it is already not working in some times!
Really I can't understand why?, but I try something, and it is work.
listen for transitionEnd not focus
try snack here
React.useEffect(() => {
if (textInputRef.current) {
const unsubscribe = navigation.addListener('transitionEnd', () => {
textInputRef.current?.focus();
})
return unsubscribe;
}
}, [navigation, textInputRef.current])
other solution work for me surround textInputRef.current?.focus(); with setTimeout with 1000 ms
React.useEffect(() => {
if (textInputRef.current) {
const unsubscribe = navigation.addListener('focus', () => {
setTimeout(() => {
textInputRef.current?.focus();
}, 1000);
})
return unsubscribe;
}
}, [navigation, textInputRef.current])
I am using the React useState hook to update a list of items. I would like for only the added/updated components to be rendered but everytime the state of of the list changes all the items in list are re-rendered.
I have followed Preventing list re-renders. Hooks version. to solve the re-render issue but it doesn't work
Can someone help me understand, what's wrong with the below code or if this is actually not the right way to do it
function App() {
const [arr, setArr] = useState([])
useEffect(() => {
//getList here returns a list of elements of the form {id: number, name: string}
setArr(getList());
}, [])
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, [arr])
return (
<div className="App">
{
arr.map((item) => {
return (
<NewComp key={`${item.id}`} item={item} clickHandle={clickHandle} />
);
})
}
</div>
);
}
const NewComp = ({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
}
The reason all your NewComp re-render is because your clickHandle function is being recreated whenever there is any change in the state arr.
This happens because you have added arr as a dependency to useCallback. This however is not required.
Once you fix it, you can wrap your NewComp with React.memo to optimize their re-renders. Also you must note that call the render function of a component is different from actually re-rendering it in the DOM.
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, []);
const NewComp = React.memo({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
});