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])
Related
I have a specific problem that is keeping me awake this whole week.
I have a parent component which has a pop-up children component. When I open the page the pop-up shows off and after 5 seconds it disappears with a setTimeout.
This pop-up has an input element in it.
I want the pop-up to disappear after 5 seconds or if I click to digit something in the input. I tried to create a timerRef to the setTimeout and closes it in the children but it didn't work.
Can you help me, please? Thanks in advance.
ParentComponent.tsx
const ParentComponent = () => {
const [isVisible, setIsVisible] = useState(true)
timerRef = useRef<ReturnType<typeof setTimeout>>()
timerRef.current = setTimeout(() => {
setIsVisible(false)
}, 5000)
useEffect(() => {
return () => clearTimeout()
})
return (
<div>
<ChildrenComponent isVisible={isVisible} inputRef={timerRef} />
</div>
)
}
ChildrenComponent.tsx
const ChildrenComponent = ({ isVisible, inputRef}) => {
return (
<div className=`${isVisible ? 'display-block' : 'display-none'}`>
<form>
<input onClick={() => clearTimeout(inputRef.current as NodeJS.Timeout)} />
</form>
</div>
)
}
You're setting a new timer every time the the component re-renders, aka when the state changes which happens in the timeout itself.
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
Instead you can put the initialization in a useEffect.
useEffect(() => {
if (timerRef.current) return;
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
return () => clearTimeout(timerRef.current);
}, []);
You should also remove the "loose" useEffect that runs on every render, this one
useEffect(() => {
return () => clearTimeout();
});
I did search for those related issues and found some solutions, but most about the lodash debounce. In my case, I create useDebounce as a custom hook and return the value directly.
My current issue is useCallback works with an old debounced value.
Here are my code snips.
//To makes sure that the code is only triggered once per user input and send the request then.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
};
useDebounce works as expected
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const debounceInput = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setValue(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
Here are screenshots with console.info to explain what the issue is.
Current quantity
Updated with onChange
I do appreciate it if you have any solution to fix it, and also welcome to put forward any code that needs updates.
This might help you achieve what you want. You can create a reusable debounce function with the callback like below.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
let timeout;
const setDebounce = (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => setDebouncedValue(newValue), delay);
};
return [debouncedValue, setDebounce];
};
And use the function on your code like this.
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const [debounceInput, setDebounceInput] = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setDebounceInput(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
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);
}}
/>
);
};
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
I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.