How to set a state array with values from TextField using onchange - reactjs

I am new to react and am trying to add string values in an array. I am using Material-UI objects.
My state has
this.state: {
roles: []
}
A button pushes an undefined element in roles, incrementing its length.
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
So now we have some length to the roles array.
The Textfield is generated with
this.state.roles.map((item, i)=> {
return (
<TextField id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
the onchange event is handled as below
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
The console.log statements generate output like
roles[0]
[undefined]
I expect
roles[0]
["somedata"]
what is going wrong here? The data does not get set in the roles array.
The whole code file is
const styles = theme => ({
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing.unit * 2
}
})
class AddModule extends Component {
constructor() {
super();
this.state = {
roles:[],
open: false,
error: ''
}
}
clickSubmit = () => {
const module = {
roles: this.state.roles || undefined
}
create(module).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({error: '', 'open': true});
}
})
}
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
render() {
const {classes} = this.props;
return (
<div>
<Button onClick={this.clickAddRole} >Add Role</Button>
{
this.state.roles.map((item, i)=> {
return (
<TextField className={classes.textField} id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
}
</div>
)
}
}

I think you're making the whole code a bit overcomplicated creating names for each input field. What I would do is change the handleRolesChange or handleChange (not really sure if you changed its name) method so that it takes the index instead of a name.
handleRolesChange = index => event => {
const { roles } = this.state;
const newRoles = roles.slice(0); // Create a shallow copy of the roles
newRoles[index] = event.target.value; // Set the new value
this.setState({ roles: newRoles });
}
Then change the render method to something like this:
this.state.roles.map((item, index) => (
<TextField
id={`roles[${index}]`}
label={`role ${index}`}
key={index}
onChange={this.handleRolesChange(index)}
/>
))

Guy I have the issue (maybe temporarily).
I an array-element is a child of the array. so changing the data in the array-element does not need setState.
So this is what I did....
handleRolesChange = name => event => {
const i = [name];
this.state.roles[i]=event.target.value;
}
I also change the Textfield onchange parameter to
onChange={this.handleRolesChange(i)}
where i is the index starting from zero in the map function.
All this works perfectly as I needed.
However, if you think that I have mutated the roles array by skipping setState, I will keep the Question unanswered and wait for the correct & legitimate answer.
Thanks a lot for your support guys.
We must try and find the solution for such basic issues. :)

Are you positive it's not being set? From React's docs:
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. If you need to set the state based on the previous
state, read about the updater argument below.
Usually logging state in the same block you set the code in will print the previous state, since state has not actually updated at the time the console.log fires.
I would recommend using React Dev Tools to check state, instead of relying on console.log.

Related

Setting state with React Context from child component

Hey y'all I am trying to use the Context API to manage state to render a badge where the it's not possible to pass props. Currently I am trying to use the setUnreadNotif setter, but it seems because I am using it in a method that loops through an array that it is not working as expected. I have been successful updating the boolean when only calling setUnreadNotif(true/false); alone so I know it works. I have tried many other approaches unsuccessfully and this seems the most straight forward. My provider is wrapping app appropriately as well so I know its not that. Any help is greatly appreciated.
Here is my Context
import React, {
createContext,
Dispatch,
SetStateAction,
useContext,
useState,
} from 'react';
import { getContentCards } from 'ThisProject/src/utils/braze';
import { ContentCard } from 'react-native-appboy-sdk';
export interface NotificationsContextValue {
unreadNotif: boolean;
setUnreadNotif: Dispatch<SetStateAction<boolean>>;
}
export const defaultNotificationsContextValue: NotificationsContextValue = {
unreadNotif: false,
setUnreadNotif: (prevState: SetStateAction<boolean>) => prevState,
};
const NotificationsContext = createContext<NotificationsContextValue>(
defaultNotificationsContextValue,
);
function NotificationsProvider<T>({ children }: React.PropsWithChildren<T>) {
const [unreadNotif, setUnreadNotif] = useState<boolean>(false);
return (
<NotificationsContext.Provider
value={{
unreadNotif,
setUnreadNotif,
}}>
{children}
</NotificationsContext.Provider>
);
}
function useNotifications(): NotificationsContextValue {
const context = useContext(NotificationsContext);
if (context === undefined) {
throw new Error('useUser must be used within NotificationsContext');
}
return context;
}
export { NotificationsContext, NotificationsProvider, useNotifications };
Child Component
export default function NotificationsPage({
navigation,
}: {
navigation: NavigationProp<StackParamList>;
}) {
const [notificationCards, setNotificationCards] = useState<
ExtendedContentCard[]
>([]);
const user = useUser();
const { setUnreadNotif } = useNotifications();
const getCards = (url: string) => {
if (url.includes('thisproject:')) {
Linking.openURL(url);
} else {
navigation.navigate(ScreenIdentifier.NotificationsStack.id, {
screen: ScreenIdentifier.NotificationsWebView.id,
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
params: {
uri: `${getTrustedWebAppUrl()}${url}`,
title: 'Profile',
},
});
}
getContentCards((response: ContentCard[]) => {
response.forEach((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
});
});
Braze.requestContentCardsRefresh();
};
return (
<ScrollView style={styles.container}>
<View style={styles.contentContainer}>
{notificationCards?.map((item: ExtendedContentCard) => {
return (
<NotificationCard
onPress={getCards}
key={item.id}
id={item.id}
title={item.title}
description={item.cardDescription}
image={item.image}
clicked={item.clicked}
ctaTitle={item.domain}
url={item.url}
/>
);
})}
</View>
</ScrollView>
);
}
Fixed Issue
I was able to fix the issue by foregoing the forEach and using a findIndex instead like so:
getContentCards((response: ContentCard[]) => {
response.findIndex((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
setUnreadNotif(false);
});
});
An issue I see in the getContentCards handler is that it is mis-using the Array.prototype.findIndex method to issue unintended side-effects, the effect here being enqueueing a state update.
getContentCards((response: ContentCard[]) => {
response.findIndex((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
setUnreadNotif(false);
});
});
What's worse is that because the passed predicate function, e.g. the callback, never returns true, so each and every element in the response array is iterated and a state update is enqueued and only the enqueued state update when card.clicked === false evaluates true is the unreadNotif state set true, all other enqueued updates set it false. It may be true that the condition is true for an element, but if it isn't the last element of the array then any subsequent iteration is going to enqueue an update and set unreadNotif back to false.
The gist it seems is that you want to set the unreadNotif true if there is some element with a falsey card.clicked value.
getContentCards((response: ContentCard[]) => {
setUnreadNotif(response.some(card => !card.clicked));
});
Here you'll see that the Array.prototype.some method returns a boolean if any of the array elements return true from the predicate function.
The some() method tests whether at least one element in the array
passes the test implemented by the provided function. It returns true
if, in the array, it finds an element for which the provided function
returns true; otherwise it returns false. It doesn't modify the array.
So long as there is some card in the response that has not been clicked, the state will be set true, otherwise it is set to false.

Set values of list of Material UI Autocompletes that fetch options dynamically

I have a Material UI Autocomplete combo-box child component class that fetches results as the user types:
...
fetchIngredients(query) {
this.sendAjax('/getOptions', {
data: {
q: query
}
}).then((options) => {
this.setState({
options: options
});
});
}
...
<Autocomplete
options={this.state.options}
value={this.state.value}
onChange={(e, val) => {
this.setState({value: val});
}}
onInputChange={(event, newInputValue) => {
this.fetchIngredients(newInputValue);
}}
renderInput={(params) => {
// Hidden input so that FormData can find the value of this input.
return (<TextField {...params} label="Foo" required/>);
}}
// Required for search as you type implementations:
// https://mui.com/components/autocomplete/#search-as-you-type
filterOptions={(x) => x}
/>
...
This child component is actually rendered as one of many in a list by its parent. Now, say I want the parent component to be able to set the value of each autocomplete programmatically (e.g., to auto-populate a form). How would I go about this?
I understand I could lift the value state up to the parent component and pass it as a prop, but what about the this.state.options? In order to set a default value of the combo-box, I'd actually need to also pass a single set of options such that value is valid. This would mean moving the ajax stuff up to the parent component so that it can pass options as a prop. This is starting to get really messy as now the parent has to manage multiple sets of ajax state for a list of its Autocomplete children.
Any good ideas here? What am I missing? Thanks in advance.
If these are children components making up a form, then I would argue that hoisting the value state up to the parent component makes more sense, even if it does require work refactoring. This makes doing something with the filled-in values much easier and more organized.
Then in your parent component, you have something like this:
constructor(props) {
super(props);
this.state = {
values: [],
options: []
};
}
const fetchIngredients = (query, id) => {
this.sendAjax('/getOptions', {
data: {
q: query
}
}).then((options) => {
this.setState(prevState => {
...prevState,
[id]: options
});
});
}
const setValue = (newValue, id) => {
this.setState(prevState => {
...prevState,
[id]: newValue
};
}
render() {
return (
<>
...
{arrOfInputLabels.map((label, id) => (
<ChildComponent
id={id}
key={id}
value={this.state.values[id]}
options={this.state.options[id]}
fetchIngredients={fetchIngredients}
labelName={label}
/>
)}
...
</>

Using Hooks + Callbacks

I am currently converting this open source template (React + Firebase + Material UI). If you look in many parts of the codebase, you'll noticed after a state variable is changed, there will then be a call back. Here is one example from the signUp method found within SignUpDialog.js file:
signUp = () => {
const {
firstName,
lastName,
username,
emailAddress,
emailAddressConfirmation,
password,
passwordConfirmation
} = this.state;
const errors = validate({
firstName: firstName,
lastName: lastName,
username: username,
emailAddress: emailAddress,
emailAddressConfirmation: emailAddressConfirmation,
password: password,
passwordConfirmation: passwordConfirmation
}, {
firstName: constraints.firstName,
lastName: constraints.lastName,
username: constraints.username,
emailAddress: constraints.emailAddress,
emailAddressConfirmation: constraints.emailAddressConfirmation,
password: constraints.password,
passwordConfirmation: constraints.passwordConfirmation
});
if (errors) {
this.setState({
errors: errors
});
} else {
this.setState({
performingAction: true,
errors: null
}, () => { //!HERE IS WHERE I AM CONFUSED
authentication.signUp({
firstName: firstName,
lastName: lastName,
username: username,
emailAddress: emailAddress,
password: password
}).then((value) => {
this.props.dialogProps.onClose();
}).catch((reason) => {
const code = reason.code;
const message = reason.message;
switch (code) {
case 'auth/email-already-in-use':
case 'auth/invalid-email':
case 'auth/operation-not-allowed':
case 'auth/weak-password':
this.props.openSnackbar(message);
return;
default:
this.props.openSnackbar(message);
return;
}
}).finally(() => {
this.setState({
performingAction: false
});
});
});
}
};
With hooks, I tried something like this within the else statement...
setPerformingAction(true)
setErrors(null), () => {...}
I'll be honest, I am not the greatest at callbacks. From what I think this is doing is then calling the following methods after the state has been set. That said, this is not correct according to eslint and I was hoping to see if anyone could help. Thanks, Brennan.
If I understand your question correctly, you would like to know how to achieve the same behavior that the class based setState callback provides, but using functional components..
Thinking in functional components is a different ballgame than thinking in class based components.. The easiest way to put it is that class based components are more imperative, while hooks/functional components are more declarative.
The useEffect hook requires a dependency array (the part at the end }, [clicks]) is the dependency array) - anytime a variable that is included in the dependency array is changed, the useEffect method is fired.
What this means is you can use useEffect in a similar fashion to a setState callback.. Hooks allow you to focus in on, and have fine-grained control over, very specific parts of your state.
This is a good thread to check out - and more specifically, a good explanation of the difference between class based (setState) and hooks based (useState) paradigms.
The following example demonstrates how to achieve something similar to "callback" behavior, but using hooks/functional components.
const { render } = ReactDOM;
const { Component, useState, useEffect } = React;
/**
* Class based with setState
*/
class MyClass extends Component {
state = {
clicks: 0,
message: ""
}
checkClicks = () => {
let m = this.state.clicks >= 5 ? "Button has been clicked at least 5 times!" : "";
this.setState({ message: m });
}
handleIncrease = event => {
this.setState({
clicks: this.state.clicks + 1
}, () => this.checkClicks());
}
handleDecrease = event => {
this.setState({
clicks: this.state.clicks - 1
}, () => this.checkClicks());
}
render() {
const { clicks, message } = this.state;
return(
<div>
<h3>MyClass</h3>
<p>Click 'Increase' 5 times</p>
<button onClick={this.handleIncrease}>Increase</button>
<button onClick={this.handleDecrease}>Decrease</button>
<p><b><i>MyClass clicks:</i></b> {clicks}</p>
<p>{message}</p>
</div>
);
}
}
/**
* Function based with useState and useEffect
*/
function MyFunction() {
const [clicks, setClicks] = useState(0);
const [message, setMessage] = useState("");
useEffect(() => {
let m = clicks >= 5 ? "Button has been clicked at least 5 times!" : "";
setMessage(m);
}, [clicks]);
const handleIncrease = event => setClicks(clicks + 1);
const handleDecrease = event => setClicks(clicks - 1);
return(
<div>
<h3>MyFunction</h3>
<p>Click 'Increase' 5 times</p>
<button onClick={handleIncrease}>Increase</button>
<button onClick={handleDecrease}>Decrease</button>
<p><b><i>MyFunction clicks:</i></b> {clicks}</p>
<p>{message}</p>
</div>
);
}
function App() {
return(
<div>
<MyClass />
<hr />
<MyFunction />
</div>
);
}
render(<App />, document.body);
p {
margin: 1px;
}
h3 {
margin-bottom: 2px;
}
h3 {
margin-top: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
You can use the setter of useState multiple times in a handler but you should pass a function to the setter instead of just setting it.
This also solves missing dependency if you create your handler with useEffect.
const [state, setState] = useState({ a: 1, b: 2 });
const someHandler = useCallback(
() => {
//using callback method also has the linter
// stop complaining missing state dependency
setState(currentState => ({ ...currentState, a: currentState.a + 1 }));
someAsync.then(result =>
setState(currentState => ({
...currentState,
b: currentState.a * currentState.b,
}))
);
},
//if I would do setState({ ...state, a: state.a + 1 });
// then the next line would need [state] because the function
// has a dependency on the state variable. That would cause
// someHandler to be re created every time state changed
// and can make useCallback quite pointless
[] //state dependency not needed
);
Note that this component can actually be unmounted before your async work finishes and would cause a warning if you call state setter when component is unmounted so you'd better wrap the setter.
Last time I told you that checking mounted is pointless because you were checking it in App component and that component never unmounts (unless you can show me some place in your code that does unmount it) but this component looks like something that may unmount at some point.
In this case if you want to run some callback function if there are any errors after performing an action, you can use useEffect in this case since react hooks do not accept a 2nd optional callback function as with setState.
you can add errors into the dependency array of useEffect and write your callback function inside useEffect. Which will potentionally mean to run some funtion if there are any errors.
performAnAction = () => {
if (actionWentWrong) {
setError(); // maintained in state using useState ( const [error, setError] = useState(false);
}
}
useEffect(() => {
// inside of this function if there are any errors do something
if(error) {
// do something which you were previously doing in callback function
}
}, [error]); // this useEffect is watching if there is any change to this error state

Dropdown select component state one step behind

I have no clue why the selected dropdown value is one step behind in the URL search params string. My url is like this: http://localhost/?dropdownsel=. Below is my code:
//App.js
//update params value
function setParams({ dropdownsel }) {
const searchParams = new URLSearchParams();
searchParams.set("dropdownsel", dropdownsel);
return searchParams.toString();
}
class App extends Component {
state = {
dropdownsel: ""
};
//update url params
updateURL = () => {
const url = setParams({
dropdownsel: this.state.dropdownsel
});
//do not forget the "?" !
this.props.history.push(`?${url}`);
};
onDropdownChange = dropdownsel => {
this.setState({ dropdwonsel: dropdownsel });
this.updateURL();
};
render() {
return (
<Dropdownsel
onChange={this.onDropdownselChange}
value={this.state.dropdownsel}
/>
);
}
}
Below is dropdownsel component code:
//Dropdownsel.js
const attrData = [{ id: 1, value: AA }, { id: 2, value: BB }];
class Dropdownsel extends Component {
onDropdownChange = event => {
this.props.onChange(event.target.value);
};
render() {
return (
<div>
<select value={this.props.value} onChange={this.onDropdownChange}>
<option value="">Select</option>
{attrData.map(item => (
<option key={item.id} value={item.value}>
{" "}
{item.name}
</option>
))}
</select>
</div>
);
}
}
export default Dropdownsel;
Thanks for formatting my code. I don't know how to do it every time when I post question. I figured it out myself. I need to make a call back function for updateURL() because the setState() is not executed immediately. so my code should be revised like below:
onDropdownChange = (dropdownsel) => {
this.setState({ dropdwonsel:dropdownsel }, ()=>{this.updateURL();
});
};
The problem occurs because this.setState is asynchronous (like a Promise or setTimeout are)
So there are two workarounds for your specific case.
Workaround #1 - using a callback
Use the callback option of this.setState.
When you take a look at the declaration of setState, it accepts an "optional" callback method, which is called when the state has been updated.
setState(updater[, callback])
What it means is that, within the callback, you have an access to the updated state, which was called asynchronously behind the scene by React.
So if you call this.updateURL() within the callback, this.state.dropdownsel value will be the one you are expecting.
Instead of,
this.setState({ dropdwonsel: dropdownsel });
this.updateURL();
Call this.updateURL in the callback.
// Note: '{ dropdwonsel }' is equal to '{ dropdwonsel: dropdwonsel }'
// If your value is same as the state, you can simplify this way
this.setState({ dropdwonsel }, () => {
this.updateURL()
});
Workaround #2 - passing the new value directly
You can also pass the new value directly as an argument of this.updateURL() (which might make testing easier as it makes you method depend on a value, which you can fake).
this.setState({ dropdwonsel });
this.updateURL(dropdownsel );
Now your this.updateURL doesn't depend on the this.state.dropdownsel, thus you can can use the arg to push the history.
//update url params
updateURL = dropdownsel => {
const url = setParams({
dropdownsel
});
//do not forget the "?" !
this.props.history.push(`?${url}`);
};

Pass ref to function with looped id?

I'm trying to create a very simple CMS that allows the user to update certain areas on the page.
I have a h3 tag where I want to be able to pass a ref to my onChange function so that I can grab it's innerHTML text (that gets changed by contentEditable) and pass on the new data that gets changed to my back-end server. However, I'm having trouble being able to grab the innerHTML (of the new data) of the correct looped h3 that wants to get changed.
I read documentation online that ref would help me with this but it only gives me an example of where it does it in the render method instead of how to pass it to a function within the ref.
In short, I want to be able to modify my h3 tag (within the cms) with new data and send it to my back-end server to upload to my db.
Also, I tried playing around with not putting it inside of a function and I manage to get access to the myRef.current however in the console it shows as null I want to be able to get access to the specified ref's blogTopic Id so I know which mapped id I'm sending to my back-end server.
I have a lot of code so I'm only going to show the part where I'm stuck on:
class Blogtopics extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.state = {
blogData: [],
blogTopic: "",
};
}
Selectblogtopics = async () => {
const blogTopics = await blogtopicsService.selectblogTopics();
this.setState({
blogData: blogTopics
});
};
editorData = (event, content) => {
let data = content.getData();
//this.setState({ blogContent: data });
};
onChange = (event, content) => {
const node = this.myRef;
//where im stuck
console.log(node);
};
render() {
const node = this.myRef;
console.log(node);
return (
{this.state.blogData.map((rows, index) => (
<div className="blogWrapper" key={uuid()}>
<div className="col-md-6">
<h3
suppressContentEditableWarning
contentEditable={this.state.isEditing}
style={
this.state.isEditing === true
? { border: "1px solid #000", padding: "5px" }
: null
}
onInput={e => this.onChange(e)}
ref={e => this.onChange(e, this.myRef)}
//onBlur={e => this.onChange(e)}
>
{rows.blog_category}
</div>
))}
);
}
}
export default Blogtopics;
onChange = (event) => {
const nodeContent = this.myRef.current.innerHTML;
console.log(nodeContent);
};
<h3 ... ref={this.myRef} onInput={this.onChange} ... >
will work. But since onInput passes target element you don't even need to use ref:
onChange = ({ target }) => {
console.log(target.innerHTML);
}
<h3 onInput={this.onChange} >

Resources