React native setState hook reopens modal - javascript

I have a weird issue with a modal in react native (expo). My modal looks like this:
const useNewCommentModal = () => {
const [showModal, setShowModal] = useState(false);
const [comment, setComment] = useState('');
const toggle = () => {
setShowModal(!showModal);
};
const NewCommentModal = () => (
<Modal visible={showModal} animationType="slide">
<View style={[t.pX4]}>
<TextInput
style={[t.bgWhite, t.p2, t.rounded, t.textLg]}
placeholder={'Post jouw reactie...'}
onChangeText={text => setComment(text)}
value={comment}
/>
</View>
</Modal>
);
return [toggle, NewCommentModal];
};
export default useNewCommentModal;
When I type the modal keeps reopening. When I remove this:
onChangeText={text => setComment(text)}
The problem goes away but obviously the state isn't updated anymore. How
come the model keeps reopening?
--EDIT--
const [toggleModal, NewCommentModal] = useNewCommentModal(
'user',
route.params.user.id
);
<NewCommentModal />

Every time your useNewCommentModal hook runs, it creates a new function called NewCommentModal which you then use as component <NewCommentModal /> (this part is very important). To React, every new NewCommentModal is different from a previous one (because you create new function each time and every render), React would run equality comparison between old and new NewCommentModal which will return false, so React will unmount old modal and mount a new one in it's place. That happens because you call function as component. So you should return not NewCommentModal component from your hook but a function that will render something instead (renderNewCommentModal) and call it not as a component but as a function ({renderNewCommentModal()}) instead. Or better yet, return only data from a hook, and use this data to render something in your main component
See my previous answer on this matter for more detailed explanation https://stackoverflow.com/a/60048240/8390411

Related

What is the point to use useCallback in a component without React.memo?

Consider example:
const Child = () => {
console.log("I did re-render!");
return null;
};
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
<Child fn={fn} />
</div>
);
};
With every state change in App (click the button), the Child component re-renders, even if the passed prop fn is wrapped with useCallback. However, if I wrap Child with React.memo, it starts to work correctly - it does not re-render when parent re-renders.
My question: What's the point of using useCallbacks without React.memo?? Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Playground: https://codesandbox.io/s/peaceful-khorana-nrojpb?file=/src/App.js
My question: What's the point of using useCallbacks without React.memo??
There is none unless you otherwise tell React to compare based on refernece down the line.
Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Yes, or something equivalent.
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Yes, unless you do something equivalent.
Just to elaborate - other than React.memo you can always wrap a sub-render with useMemo:
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
const child = useMemo(() => <Child fn={fn} />, [fn]);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
{child}
</div>
);
};
Or "roll your own" with useRef+useEffect or use class components and override shouldComponentUpdate or inherit from React.PureComponent.

Toggle component without changing state

I have a component Scroller which I don't control which takes in data as a prop.
This data is a list of objects. Within object, one of the keys takes in a function.
This component has ability where upon clicking on the square, I am meant to show a new component (like a pop up).
The component Scroller which I don't control taking in the data prop.
<Scroller
data={getData(allData)}
/>
This is the data being passed in. content is a list of objects.
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: (() => {
setClicked(true); // this is the line which resets the scroll
}),
}
));
So this works as intended. Upon clicking, the new pop up content shows. This is due to state change via the setClicked function.
The issue is that this Scroller component has a scroll option. So user could have scrolled pass a a block (0) like following image.
But the moment I click the button to show the popup, it resets the scroll position back to 0 like following. Instead of remaining in position as above.
This scroll reset is the issue.
This is being caused by the call to setClicked function. It doesn't matter if I do anything with it. As long as I call it, it resets.
Showing the popup component is not the issue. The mere call to setClicked is the issue.
Thus wondering if there a way I could toggle showing the pop up component without having to set state?
Or a way to maintain the scroll position without resetting the scroll.
Note that in this instance I am using hooks. It is the same outcome if I use Redux. Please advice.
This is my component which I can control.
import React, { Fragment } from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const { allData } = data;
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
return (
<Fragment>
<Scroller
data={getData(allData)}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</Fragment>
);
};
export default MyComponent;
Explanation:
Each time setClick is called, the value of isClicked is changed, which causes MyComponent to be reevaluated. Since allData is initialized inside MyComponent, it will be reinitialized each time MyComponent is reevaluated. Another issue is that the data being sent to Scroller is the result of a function that takes in allData. Each time MyComponent is reevaluated, that function will run again and return a new array instance given the new allData instance. This means that every time MyComponent reevaluates, Scrollbar gets a new instance of data, causing anything that consumes data inside of Scrollbar to also be reevaluated.
Solution:
My suggestion would be to utilize react's useMemo hook (docs: https://reactjs.org/docs/hooks-reference.html#usememo) to 'memoize' the data going into Scroller:
import React from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const scrollerData = React.useMemo(()=> {
return data.allData.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
},[data])
return (
<>
<Scroller
data={scrollerData}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</>
);
};
export default MyComponent;
Also fun fact, <> is shorthand for React's Fragment
The problem could be that each time you click the component your Scroller gets a different reference of the data and because of that, it calls lifecycle methods that cause your performance issue.
If you will send the same props ( same reference ) to Scroller it should not call any lifecycle method which propably causes your problems.
import React, { Fragment, useMemo, useState } from 'react'
import Scroller from 'comp-external-lib'
import PopUpComponent from './PopUpComponent'
const MyComponent = props => {
const [isClicked, setClicked] = useState(false)
const { allData } = props.data
const getData = content =>
content.map((c, i) => ({
header: c.header,
customOnClick:
c.customOnClick &&
(() => {
setClicked(true)
})
}))
const scrollerData = useMemo(() => getData(allData), [allData])
return (
<Fragment>
<Scroller data={scrollerData} />
{isClicked && <PopUpComponent />}
</Fragment>
)
}
export default MyComponent
You are calling getData on every render cycle and thus causing a reset of the state:
data={getData(allData)}
The solution will be to wrap the getData function with a useCallback hook:
const getData = useCallback((content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
)),[]);

React Native To-Do app does not update state on click but does after another state updates

I tried making a to-do app using React-native. It is a blank react native project that uses typescript and expo. I'm on linux.
The state containing the todo array gets updated it should but the view is not rendered accordingly untill I click the input button and change the text state.
I have 3 functions to modify the states - textChange (for updating the todoText state), handleAddTask (for updating the array containing tasks), rmTask ( removes item from task array)
const [task, setTask] = useState('');
const [taskItems, setTaskItems] = useState(["Write a task"]);
const textChange = (text: string) => {
setTask(text)
}
const handleAddTask = () => {
taskItems.push(task)
setTaskItems(taskItems)
setTask('')
}
const rmTask = (index: number) => {
taskItems.splice(index, 1);
setTaskItems(taskItems)
}
And the components where the functions are affecting
taskItems.map((item, index) => {
return (
<TouchableOpacity
key={index}
onPress={()=> rmTask(index)}
>
<Task text={item} />
</TouchableOpacity>)
})
const handleAddTask = () => {
// taskItems.push(task) <-- Dont mutate state like this !
setTaskItems([...taskItems, task]). <-- You should use it like this way.
setTask('')
}
You are mutating the state value without using the setter function. Thats why Its not working correctly. And when you write something new to task input, UI gonna re-render and display the correct thing. Because textChange function implementation correct and update the state.
In two places you are directly manipulating the state rather than using setState.
You should do this way:
//taskItems.splice(index, 1);
//setTaskItems(taskItems)
// -should be
setTaskItems([...taskItems].splice(index, 1))
and
//taskItems.push(task)
//setTaskItems(taskItems)
// -should be
setTaskItems([...taskItems, task])

cannot update a component from inside the function body of a different component - React Native?

I have a child component "Text Input" and passes the value to as a prop like this
export default function MobileInput(props) {
const [mobileNumber, setMobileNumber] = React.useState('');
return (
<View style={styles.inputBox}>
<TextInput
value={mobileNumber}
onChangeText={(number) => setMobileNumber(number)}
onEndEditing={props.saveMobileNumber(mobileNumber)} // here
/>
</View>
);
}
In Parent, I got the value from child
const [mobile, setMobile] = useState('');
const getMobile = (number) => {
number ? setMobile(number) : null; // here's I got this warnning
console.log('getMobile-number-from-child', number);
};
const reSendMobile = () => { // other function I want to call passed on mobile number I got from child component
if (mobile?.length) {
alert('Done');
setReSend(false);
setSeconds(10);
} else {
alert('Please write your number before press send!');
}
};
<MobileInput saveMobileNumber={getMobile} />
I see this issue But I'm already using React 16.13.1
TextInputs property onEndEditing accepts a function that is called when text input ends.. Instead of a function, you are passing the result of your props.saveMobileNumber function that is called when the component renders. Try passing a function that calls saveMobileNumber instead:
onEndEditing={() => props.saveMobileNumber(mobileNumber)}
Your code will be much easier to read/debug if you avoid keeping the same state in multiple components. You can pass mobile and setMobile to the child through props and avoid having to create separate state for the same data.
Try this:
<View style={styles.inputBox}>
<TextInput
value={mobileNumber}
onChangeText={(number) => setMobileNumber(number)}
onEndEditing={() => props.saveMobileNumber(mobileNumber)} // change here
/>
</View>
The event onEndEditing accepts a function call
Just update to call a arrow function :
onEndEditing={() => props.saveMobileNumber(mobileNumber)}
For me, i was updating activity title outside the useEffect hook. When i moved the code
into useEffect hook, the error just gone.

How to force a functional React component to render?

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().
🎉 You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.
Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.
Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
Simplest way 👌
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)
I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};
Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()
If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated
This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}
The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);
None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}
If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.
For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Categories

Resources