setState asynchronous while uploading image - reactjs

I need to upload a file to the server for that i have given fileChangedHandler function for onClick event. but in that function setState is skipping because of asychronous operation.
i am giving my code below.
class MultiSelectField extends React.Component {
constructor(props) {
super(props);
this.state = { //image: '',
selectedFile: null};
this.fileChangedHandler = this.fileChangedHandler.bind(this);
this.fileUploadHandler = this.fileUploadHandler.bind(this)
}
fileChangedHandler = (event) => {
this.setState({selectedFile:event.target.files[0]}) // asynch and skipping
console.log(this.state.selectedFile)
this.forceUpdate()
}
fileUploadHandler = () => {
const fd = new FormData()
fd.append('myFile', this.state.selectedFile, this.state.selectedFile.name)
axios.post('my-domain.com/file-upload', formData)
console.log("selected"+JSON.stringify(this.state.selectedFile))
console.log("form data : "+fd)
}
render() {
return (
<div>
<input type="file" onChange={this.fileChangedHandler} ref="file" />
<button onClick={this.fileUploadHandler}>Upload!</button>
</div>
);
}
}
i am not getting the data while consoling this.state.selectedFile it showing null.
Thanks in advance

You can hide Upload Button initially and show it after selected file is set to state in a callback.
fileChangedHandler = (event) => {
this.setState({selectedFile:event.target.files[0], showUploadButton:true}, () =>{
// file has been selected
console.log(this.state.selectedFile) <-- console.log in callback
}) // asynch and skipping
this.forceUpdate()
}
render
{this.state.showUploadButton && <button onClick={this.fileUploadHandler}>Upload!</button>}
Now upload button will appear once the file has been set to state, then you'll get it.
However your problem relies here -
this line should be inside the callback after you set the state as I mentioned above.
console.log(this.state.selectedFile)
Callback of setState method makes sure that state has been set.
update
fileChangedHandler = (event) => {
this.setState({selectedFile:event.target.files[0], showUploadButton:true}, () => {
console.log(this.state.selectedFile)
this.forceUpdate()
})
}
so why this worked?
React setState method is asynchronous which is why you receive confirmation via callback when a state is set in component.
Another question may arise in your mind that why its asynchronous whether it does not call any external resource to set state (API or server call).
The answer in simple words is -
Because on every state change in React component it re-renders render() method or Virtual DOM. Now suppose you have 100s of states to be set in a component on one click, Then this should be rendering virtual DOM 100s of times. Which react actually doesn't do.
It creates batches of states. Say bifurcation of 100s of state into 2-3 batches. Now from UI perspective it accepts multiple states at once but set them in batches by grouping the states into a few batches that keeps UI free and in non blocking state.
That is why the term asynchronous is used to setState.

Using setState callback to call forceUpdate is unneccessary - setting state will force update (new render) ;)
Callback is usable when setting state sholud fire some actions after change is done - in this case following action is trigerred manually. KISS & readable ;)
fileChangedHandler = (event) => {
this.setState({selectedFile:event.target.files[0]})
}
you can disable upload button with simple condition
render() {
return (
<div>
<input type="file" onChange={this.fileChangedHandler} ref="file" />
<button onClick={this.fileUploadHandler} disabled={!this.state.selectedFile}>Upload!</button>
</div>
);
}
or hide it
render() {
return (
<div>
<input type="file" onChange={this.fileChangedHandler} ref="file" />
{!this.state.selectedFile &&
<button onClick={this.fileUploadHandler}>Upload!</button>
}
</div>
);
}

Related

Retrieving latest state from functional child component using callbacks

Coming from Vue and diving into React I seem to struggle with the concept of the hooks / component lifecycle and data flow. Where in Vue I could solve my issues using v-model in React I struggle to do so. Basically:
What I intend to do is : have a parent component which is a form. This component will have several child components where each are fragments of the form data. Each child component manages its own state. The parent component has a submit button that should be able to retrieve the values from all child components.
In a nutshell, my approach is: have a functional component to manage part of said form using state hooks. This "form fragment"-component can broadcast a change event broadcastChange() containing the updated values of its inputs. In the parent I have a submit button which invokes this broadcastChange() event from the child using a ref.
The problem I am running into is that I am always getting the default values of the child state. In the below example, I'd always be getting foo for property inputValue. If I were to add a submit button inside the child component to invoke broadcastChange() directly, I do get the latest values.
What am I overlooking here ? Also, if this is not the React way to manage this two-way communication, I'd gladly hear about alternatives.
Parent code:
function App() {
const getChildChangesFn = useRef( null );
const submitForm = e => {
e.nativeEvent.preventDefault();
getChildChangesFn.current(); // request changes from child component
};
const handleChange = data => {
console.log( data ); // will always list { inputValue: "foo" }
};
return (
<form>
<child getChangeFn={ getChildChangesFn } onChange={ handleChange } />
<button type="submit" onClick={ () => submitForm() }>Save</button>
</form>
);
}
Child code:
export default function Child( props ) {
const [ inputValue, setInputValue ] = useState( "foo" );
useEffect(() => {
// invoke inner broadcastChange function when getChangeFn is triggered by parent
props.getChangeFn.current = broadcastChange;
}, []);
const broadcastChange = () => {
props.onChange({ inputValue });
};
render (
<fieldset>
<input
type="text"
value={ inputValue }
onChange={ e => setInputValue( e.target.value ) }
/>
</fieldset>
);
}
You need to leave Vue behind and make your thinking more React-y. Rather than trying to manage your state changes imperitavely by 'broadcasting' them up, you need to 'lift your state' (as they say) in to your parent and pass it down to your children along with change handlers.
A simple example:
export default function App() {
const [childState, setChildState] = useState(false);
const onChildClick = () => setChildState((s) => !s);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<ChildComponent childState={childState} onClickHandler={onChildClick} />
</div>
);
}
const ChildComponent = ({ childState, onClickHandler }) => {
return (
<button onClick={onClickHandler}>
State is {childState ? "true" : "false"}
</button>
);
};
Sandbox here
You have to uplift the state to parent component, or use a state manager like redux. In your case you are already passing down an onChange function, which should do what you need. But on parent component you need to manage the state, and store changes in the state, and pass it down to components as props.
An alternative to redux is mobx, which is a reactive library, in your case, sounds like you are familiar with reactive components, might fit you better. Other alternatives are using the Context that comes with react, and react-query is also a solid alternative, as it also handles async api calls.

setInterval with updated data in React+Redux

I have setInterval setup to be working properly inside componentDidMount but the parameters are not updated. For example, the text parameter is the same value as when the component initially mounted, despite being changed in the UI. I've confirmed text's value is correctly updated in Redux store but not being passed to this.retrieveData(text). I suspect the const { text } = this.props set the value in componentDidMount, which forbids it from updating despite it being different. How would I go about this issue?
Code below is an example, but my real use-case is retrieving data based on search criteria. Once the user changes those criteria, it will update with the new result. However, I'm unable to pass those new criteria into componentDidMount so the page would refresh automatically every few seconds.
class App extends React.Component {
componentDidMount() {
const { text } = this.props //Redux store prop
setInterval(() => this.retrieveData(text), 3000)
}
retrieveData = (text) => {
let res = axios.post('/search', { text })
updateResults(res.data) //Redux action
}
render() {
const { text, results } = this.props
return (
<input text onChange={(e) => updateText(e.target.value)} />
<div>
{results.map((item) => <p>{item}</p>}
</div>
)
}
}
Because you are using componentDidMount and setTimeout methods your retrieveData is called only once with initial value of the text. If you would like to do it in your current way please use componentDidUpdate method which will be called each time the props or state has changed. You can find more information about lifecycle here https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/.
If you would like to use setInterval just like in the question, you just need to access props inside of retrieveData method instead of using an argument.
retrieveData = () => {
let res = post("/search", { text: this.props.text });
updateResults(res); //Redux action
};
You can find working example for both cases here https://codesandbox.io/s/charming-blackburn-khiim?file=/src/index.js
The best solution for async calls would be to use some kind of middleware like https://github.com/reduxjs/redux-thunk or https://redux-saga.js.org/.
You have also small issue with input, it should be:
<input type="text" value={text} onChange={(e) => updateText(e.target.value)} />

React: React-bootstrap, calling function only after user has finished typing

I am utilizing React-Bootstrap for a simple text box component. Right now, the callback is called every time the user edits the textbox, hence it is called back when every letter is called
<Form.Group controlId="Id">
<Form.Label>Id</Form.Label>
<Form.Control style = {{marginLeft: 10}} name="Idstate" onChange={this.handleChange} defaultValue = {this.state.idstate}/>
</Form.Group>
The code above is my form group
handleChange = (event) => {
event.persist()
const name = event.target.name
if (name == "Idstate"){
this.setState({idstate: event.target.value})
this.state.idstate = event.target.value;
Apiservice.updatetabletopmenudata(this.tableid, event.target.value, "idstate")
}
console.log(this.state)
}
This results in a really laggy process, and sometimes the data isn't saved properly
Is there any way for the callback to be executed only after the user has finished editing the text box?
This is because every time a state changes the component re-renders.So, you can avoid unnecessary renders by creating a variable in the state state = {fromOnChange: false} which should always be false and whenever there is a change in the input text field, handle like below
handleChange(event) {
this.setState({
fromOnChange: true,
});
}
And when the state updates keep a life cycle method that checks wheather to render the component or not :
shouldComponentUpdate(nextProps, nextState) {
if (!nextState.fromOnChange) {
return true;
} else {
return false;
}
}
After that whenever user had finished editing the text fields and submits the data then change the fromOnchange: false so that component renders only on onsubmit.
handleSubmit(event) {
//Your API/function call goes here
// and on success change the state so that it renders
this.setState({
fromOnChange: false
});
}
First, fixing errors before performance:
Using defaultValue would create an uncontrolled component - only the default value will be set from React, then ignore any state changes.
If you need to control the value from React, please use value:
<Form.Control ... value={this.state.idstate} />
Also, never modify state directly, this.setState({idstate: event.target.value}) is enough, no need for this.state.idstate = ... - but the operation is asynchronous (see the link above), so console.log(this.state) is meaningless inside the handleChange - you can console.log inside render or just use React DevTools to view the current state (both for classes and hooks).
Second, performance:
React state update is NOT slow by itself, it's pretty optimized. If you want to delay a slow Apiservice, you may try following:
handleChange = (event) => {
const name = event.target.name
const value = event.target.value
if (name == "Idstate"){
this.setState({idstate: event.target.value})
setTimeout(() =>
Apiservice.updatetabletopmenudata(this.tableid, value, "idstate")
)
}
}

How to update and use state by event handler in React

I have component Searcher with function SearchArticle() which is correctly using this.state.search with DEFAULT value after component mounts (console displaying Searching...:DEFAULT) . But when I update this.state.search with handleKeyPress(e) them same function SearchArticle() is using prev state before it updates to e.target value (console displaying Searching...:DEFAULT again). Not sure how to fix it.
class Searcher extends Component {
constructor(props) {
super(props);
this.state = {
article: [], search: "DEFAULT"
};
}
searchArticle() {
console.log('Searching...: ', this.state.search)
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.setState({search: e.target.value});
this.searchArticle();
}
}
componentDidMount() {
this.searchArticle();
}
render() {
return (
<div className="row">
Search: <input onKeyPress={this.handleKeyPress} type="text" />
</div>
)
}
}
Most likely the state hasn't updated by the time the console.log is executed. This is because setState() is asynchronous.
So try this instead:
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.setState({search: e.target.value}, () => {
this.searchArticle();
});
}
}
I moved your searchArticle() into the setState() callback. This will guarantee its execution after the state has actually updated.
Read more about setState() here.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.
[...]
The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered.

React Native setState

I am trying to change my state from the render function but it does not work.
render() {
return (
<View>
// some code
{this._renderModal()}
</View>
)
}
_renderModal() {
let channel = this.props.pusher.subscribe('my-channel');
channel.bind('my-event', data => {
this.setState({
requestedToEnter: false
});
this.props.navigation.getNavigator('root').push(Router.getRoute('order', { restaurant_id }));
});
return (
<Modal
animationType={'slide'}
transparent={false}
visible={this.state.requestedToEnter}
onRequestClose={this._handleEnter}>
// some components
</Modal>
)
}
When I receive the my-event on my-channel it fires the function that is bind to it. The setState method does not change the state (or it does but do not re-render the component). I think it's because I am using the setState method in the event function.. How I can close the Modal when I receive an event from pusher?
This is a bad practice to call this.setState function in your render() as each time setState is called, the component will render itself with the updated state values binding to your components / subcomponents.
Alternatively, you can create a state variable
this.state = {
isModalOpen: true
}
And put your pusher event listener in componentDidMount() and update your state variable value in the callback. Then, control your modal component visibility with the state variable:
<Modal
animationType={'slide'}
transparent={false}
visible={this.state.isModalOpen}></Modal>

Resources