Reset isLoading property of NativeBase button when on-press handler fails - javascript

I have made a "one shot" button wrapper component which intercepts the onPress event, sets the isLoading property and then calls the original onPress handler. This sets the button to the disabled loading spinner state while a slow API call is made, provides nice UI feedback and possibly prevents the user from double-clicking.
The original onPress handler has form-field validation, and if this fails I want to cancel the process and let the user correct the input data. So I am returning false to indicate this, which my wrapper catches, however I find I cannot set the button's isLoading back to false - it doesn't change the button state nor remove the disabled, so it's stuck spinning forever.
My button wrapper is this:
import React, {useState} from "react"
import {StyleSheet} from "react-native";
import {Button as ButtonNB, IButtonProps, useTheme} from "native-base"
interface IMyButtonProps extends IButtonProps {
oneShot?: boolean
}
const Button = (props: IMyButtonProps) => {
const [isLoading, setIsLoading] = useState(false)
const {colors} = useTheme()
return <ButtonNB px="25"
style={styles.button}
isLoading={isLoading} spinnerPlacement="end" isLoadingText="Saving..." backgroundColor={colors['primary']['500']}
{...props}
onPress={event => {
if (props.oneShot) setIsLoading(true)
if (props.onPress) {
if(!props.onPress(event)) {
console.debug('cancelled loader')
setIsLoading(false) // <--- DOESN'T WORK
}
}
}}
/>
}
export default Button
Calling code simplified:
<Button
onPress={() => onSave()}
oneShot={true}
testID="prepare-submit-button"
>
{saveSubjectButtonText}
</Button>
async function onSave(){
// on validation failure, just stop and return false
if(!validate()){
return false
}
else {
// do api stuff...
// update local state...
navigation.navigate('Home')
}
}
When validation fails, I do get the 'cancelled loader' log, but the setIsLoading(false) has no effect.
I am viewing in iOS, package versions:
"native-base": "~3.4",
"react": "17.0.2",
"react-dom": "~17.0.2",
"react-native": "0.67.4",
Their documentation: https://docs.nativebase.io/button#h3-loading
I've looked at their issues: https://github.com/GeekyAnts/NativeBase/issues?q=is%3Aissue+isLoading+button+is%3Aclosed

I tried an alternative approach which worked, and now I think I have realised the original problem. I think because the setIsLoading(true) in the onPress handler is re-rendering the button due to state change, the remainder of the function closure is the old state scope when the button wasn't spinning anyway.
So this was nothing to do with NativeBase nor ReactNative but a React gotcha which I think has caught me out before. Each time the render function is called, a new scope becomes active, and even though the old scope may still be finishing running code threads in memory they aren't "attached" to the display/DOM any more. At least that's how I picture it.
I changed it to use reference functions to be able to call the "setLoading" from the parent component. Thanks to this answer: Call child function from parent component in React Native
My button component is now defined like this:
import React, {forwardRef, useImperativeHandle, useState} from "react"
const Button = (props: IMyButtonProps, ref) => {
const [isLoading, setIsLoading] = useState(false)
useImperativeHandle(ref, () => ({
cancelLoading: () => { setIsLoading(false) },
}))
...
export default forwardRef(Button)
and called like this from the parent view:
const submitButtonRef = useRef()
async function onSave(){
// on validation failure, tell the button to cancel loading
if(!validate()){
submitButtonRef.current.cancelLoading()
return
}
...
<Button
onPress={() => onSave()}
ref={submitButtonRef}
And now if the validation fails, the spinner is stopped and you can click the button again.

Related

Invalid hook call error when trying to set state

I have a scenario where I am forced to call a trigger method to show a modal from two different places, one using a hotkey combination and another by clicking on a toolbar button. In order to do so I have the following code, where I call the triggerCustomLinkModal to set the state but then I am hit with the Invalid Hook call error.
import { useState, useCallback, useEffect } from "react"
import { Dialog } from "#blueprintjs/core"
const useLocalState = () => {
const [isShown, setIsShown] = useState(false)
const setState = useCallback((state) => {
setIsShown(state)
})
const getState = useCallback(() => {
return isShown
})
return {
setState,
getState
}
}
export const CustomLinkModalUI = () => {
const { getState } = useLocalState()
return (
<>
<Dialog isOpen={getState()} />
</>
)
}
export const triggerCustomLinkModal = () => {
const { setState } = useLocalState()
setState()
}
Expanding from Chris answer in the comments ( You can't use hooks outside React components. -> so you can't call useLocalState() inside triggerCustomLinkModal since triggerCustomLinkModal is not a React component ):
You don't really need the useCallback hook or even the functions itself. Aaccording to react docs :
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
This also means that using useCallback hook to set a state it doesn't really make sense (because useCallback role is just to return a memoized callback)
What you basically need is a state set up in the closest parrent component and pass the setIsShown as a prop as well as the isShown function.
Your current implementation, even if it weren't for the error, it wouldn't refer to the same state since on each useLocalState() you are initializing a fresh new state (so you are not pointing to the same state in CustomLinkModalUI and triggerCustomLinkModal)

Why the button event doesnt increment the counter?

I tried to increment the counter in the test but when i press the button the value doesnt change. I used the fireEvent from React testing library and React test utils but the value still in 10.I use react 18.
CounterApp:
import {useState} from "react";
import PropTypes from "prop-types";
const CounterApp = ({value=10})=>{
const [counter,setCounter] = useState(value);
const handleAdd= ()=>{
setCounter(counter+1);
}
const handleSubstract = ()=>{
if(counter>0){
setCounter(counter-1);
}
}
const handleReset = ()=>{
setCounter(0);
}
return(
<>
<h1>CounterApp</h1>
<h2>{counter}</h2>
<button onClick={handleAdd}>+1</button>
<button onClick={handleSubstract}>-1</button>
<button onClick={handleReset}>Reset</button>
</>
);
}
CounterApp.propTypes={
value: PropTypes.number.isRequired
}
export default CounterApp;
And the test archive:
import { create} from "react-test-renderer";
import CounterApp from "../CounterApp";
import '#testing-library/jest-dom';
import ReactTestUtils from 'react-dom/test-utils';
import {fireEvent} from "#testing-library/react";
describe("Test in counterApp",()=>{
test("Should be increment the count",()=>{
const component = create(<CounterApp value={10}/>);
const values= component.root;
const button=values.findAllByType("button").at(0).props;
const counter = values.findByType("h2").props.children;
ReactTestUtils.Simulate.click(button);
expect(counter).toBe("11");
})
})
You should format your component. Otherwise it's hard to read and you'll get issues because of that.
I couldn't understand if it works fine on a manual test, so not sure if the issue is on the testing or the component itself.
When using the setter in useState you have a callback, so instead of using the getter, you should do:
const handleAdd = () => {
setCounter(prev => prev + 1);
}
For the testing you should use an id to better identify the button, not the type.
You made a mistake to update state variable using the previous state value.
ReactJS setState()
All the React components can have a state associated with them. The state of a component can change either due to a response to an action performed by the user or an event triggered by the system. Whenever the state changes, React re-renders the component to the browser. Before updating the value of the state, we need to build an initial state setup. Once we are done with it, we use the setState() method to change the state object. It ensures that the component has been updated and calls for re-rendering of the component.
setState({ stateName : updatedStateValue })
// OR
setState((prevState) => ({
stateName: prevState.stateName + 1
}))
So you should use like the following.
const handleAdd= ()=>{
setCounter(prev => prev+1);
}
const handleSubstract = ()=>{
if(counter>0){
setCounter(prev => prev-1);
}
}

React - call function in child component if parent state changes

I've got an invisible fullscreen event processing component that catches and processes all mouse events. On certain events I want to call a particular function in a child component. I created a state variable in the parent component. I am listening with use effect in the child component if there are some changes to the state variable in the parent component. However the effect hook is being called to often. Is there a better way of doing this? In particular I want to set drawing=true on mouse down and drawing=false on mouse up. If drawing transitions from true to false I want so save something in the database. Therefore it's important that the function in useState is being called only once.
I have these two possible solutions.
If you want useEffect not to be called in the first render, you have to use a custom hook, probably called useUpdateEffect. Here is a usage guide from react-use package
use useImperativeHandle to run a function in child from parent.
example:
import React, { forwardRef, useRef } from 'react';
export default function Parent() {
const ref = useRef<Ref>(null!);
const handleCallFunction = (): void => ref?.current?.functionInChild();
return (
<div>
<button onClick={handleCallFunction}>
call a function in child from parent
</button>
<Child ref={ref} />
</div>
);
}
type Ref = {
functionInChild: () => void;
};
type Props = {};
const Child = forwardRef<Ref, Props>((props, ref) => {
const functionInChild = () => {
console.log('called from parent');
};
React.useImperativeHandle(ref, () => ({ functionInChild }));
return <div>Child!</div>;
});
You have to put this in your code
useEffect(()=>{
/*Query logic*/
console.log('i fire once');},[your_variable]);
you_variable which you want to change when this variable changes his value

Redux state update doesn't map to props immediately/synchronous. Best practice?

My issue is the following:
redux state updates won't be mapped to the props immediately. This does not refer to useState of react. I'm referring to react-redux, where the state updates should be synchronous, afaik. I know they could be batched, but not sure if that is relevant here. I assume the update is happening immediately, but the mapping is not. I don't know how to get around this. I tried to replicate the error in a "clean" environment. Here is the Component:
import React from 'react';
function MyComponent({ title, setTitle }) {
const handleclick = e => {
console.log(title);
setTitle("I don't get it.");
console.log(title);
};
return <p onClick={handleclick}>{title}</p>;
}
export default MyComponent;
Here is the ComponentContainer:
import React from 'react';
import { connect } from 'react-redux';
import { setTitle } from '../../reducers/customReducer/actions.js';
import MyComponent from './MyComponent.jsx';
export const MyComponentContainer = props => {
return <MyComponent {...props} />;
};
const mapStateToProps = state => {
return {
title: state.customData.title
};
};
const mapDispatchToProps = dispatch => {
return {
setTitle: newTitle => dispatch(setTitle(newTitle))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MyComponentContainer);
I expect the second console.log to output the value I don't get it. But it doesn't. Though, the render update happens properly. Meaning the innerHTML of the p element will be updated and rendered accordingly "a bit later".
How do I make it, that I can access the updated value instantly? This is causing an issue in my real world application. I'm not trying to simply print some value there, but rather, change a value and then call another function, that based on the updated value performs some action. Since the value isn't changed/mapped instantly, it won't work.
Am I doing something that shouldn't be done?

Closing Multiple React Components - one after another

I read through a lot of "close react component on escape / outside click" threads and am able to close all opened react components at once.
But: When 2 popups are opened, i want to first close the one "on top", then the one behind it, not both at the same time. Here is a pic of the application:
Screenshot Application with Popups to close
My code for a custom hook to close the components:
import { useRef, useEffect } from 'react';
export const useOutsideClick = (ref: any, callback: any, when: boolean) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
});
const handler = (e: any) => {
// check if target event is within the ref
if (ref.current && !ref.current.contains(e.target)) {
console.log('Handler handling states!');
savedCallback.current();
}
};
useEffect(() => {
if (when) {
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
}
}, [when]);
};
With the useRef i am creating a connection to a div that needs to be closed an call the useOutsideClick() function.
Any ideas how i can achieve closing one component after another?
Thanks & Greetings!
EDIT: The thing is, i can also close just a single component. The problem is, that like this, the application doesnt know when i click on some other component, it just notices the click when i click on the space where no component is rendered.

Categories

Resources