So quick and short of the problem. I'm building a step wizard tool for users to create custom templates. There are multiple steps that take text input from the user, my application knows which step the user is entering data on and assigns a stepid to that piece of text.
The problem I'm having is putting that data in state so that I can post it to an API based on a specific format. Here is an example of what I want that data to look like:
state: {
steps : [
{templateStepId : 'this will be the number' , data: 'this will be
the message'},
{templateStepId : 'this will be the number' , data: 'this will be
the message'} //there could be multiple text inputs each
corresponding with a different stepId
]
Here is an example of my code:
addMsg = (msg , stepId) =>{
let message = this.state.message
message[stepId] = msg
this.setState({message})
this.setState( prevState => ({
steps: [...prevState.steps, {templateStepId: stepId, data : msg}]
}));
}`
Ignore the first setState, thats for something else.
But here is where my text is coming from in another component:
handleChange = (text, type) =>{
this.props.addMsg(text, type)
}
render() {
return (
<MuiThemeProvider>
<React.Fragment>
<AppBar title = {this.props.text}/>
<TextField
placeholder = {this.props.text}
onChange = {(e) => {this.handleChange(e.target.value , e.target.name)}}
name= {this.props.templateStepId}
id = 'msg'
/>
<Navigation {...this.props} ></Navigation>
</React.Fragment>
</MuiThemeProvider>
)}
I've also tried this as a solution for my add message component:
let newStep = {templateStepId : stepId , data: msg}
this.setState({
steps : [newStep]
})
The previous code here works well and gives me the state object I want, but as soon as the user moves to another text input component it gets completely overwritten.
Edit per request, here is where the component is being called within the step wizard. As you can see there is just the one text input component called message. This component could be called multiple times depending on the stepId coming from the API which is why the stepId changes for each input.
<MuiThemeProvider>
<React.Fragment>
<StepWizard >
{
this.props.templateSteps.map((data, idx) => {
if (data.stepTypeId === "dc448967-7fad-42cf-8706-bbe1d124ceac") {
return <Message
addMsg = {this.props.addMsg}
text = {data.name}
templateStepId = {data.templateStepId}
message = {this.props.message}
startOver = {() => {this.props.startOver()}}
key={idx}
/>
Here is my state from the console in devtools :
So as you can see, it's creating a new object for each letter input. I would appreciate any help at this point as I'm beating my head against the wall trying to figure this out!
handleChange is getting called every time you make a change to the input. This is adding one more step to the state. You could check if the stepId is existing in the state and add data to that stepId only.If the stepId is not present you can add a record.
function getStepIdIndex(arr, id) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].templateStepId === id) {
return i;
}
}
return -1;
}
let addMsg = (msg, stepId) => {
let stepIdIndex = getStepIdIndex(this.state.steps, stepId);
if (stepIdIndex === -1) {
this.setState(prevState => ({
steps: [...prevState.steps, {
templateStepId: stepId,
data: msg
}]
}));
} else {
this.state.steps[stepIdIndex].data=msg;
this.setState({steps:[...this.state.steps]});
}
}
Related
I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.
So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.
I would like to use the awesome react-widgets DropDownList to load records on demand from the server.
My data load all seems to be working. But when the data prop changes, the DropDownList component is not displaying items, I get a message
The filter returned no results
Even though I see the data is populated in my component in the useEffect hook logging the data.length below.
I think this may be due to the "filter" prop doing some kind of client side filtering, but enabling this is how I get an input control to enter the search term and it does fire "onSearch"
Also, if I use my own component for display with props valueComponent or listComponent it bombs I believe when the list is initially empty.
What am I doing wrong? Can I use react-widgets DropDownList to load data on demand in this manner?
//const ItemComponent = ({item}) => <span>{item.id}: {item.name}</span>;
const DropDownUi = ({data, searching, fetchData}) => {
const onSearch = (search) => {
fetchData(search);
}
// I can see the data coming back here!
useEffect(() => {
console.log(data.length);
}, [data]);
<DropDownList
data={data}
filter
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
Got it! This issue is with the filter prop that you are passing to the component. The filter cannot take a true as value otherwise that would lead to abrupt behavior like the one you are experiencing.
This usage shall fix your problem:
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
Working demo
The entire code that I had tried at codesandbox:
Warning: You might have to handle the clearing of the values when the input is empty.
I thought that the logic for this was irrelevant to the problem statement. If you want, I can update that as well.
Also, I added a fakeAPI when searchTerm changes that resolves a mocked data in 2 seconds(fake timeout to see loading state).
import * as React from "react";
import "./styles.css";
import { DropdownList } from "react-widgets";
import "react-widgets/dist/css/react-widgets.css";
// Coutesy: https://usehooks.com/useDebounce
import useDebounce from "./useDebounce";
interface IData {
id: string;
name: string;
}
const fakeAPI = () =>
new Promise<IData[]>((resolve) => {
window.setTimeout(() => {
resolve([
{
name: "NA",
id: "user210757"
},
{
name: "Yash",
id: "id-1"
}
]);
}, 2000);
});
export default function App() {
const [state, ss] = React.useState<{
searching: boolean;
data: IData[];
searchTerm: string;
}>({
data: [],
searching: false,
searchTerm: ""
});
const debounceSearchTerm = useDebounce(state.searchTerm, 1200);
const setState = (obj: Record<string, any>) =>
ss((prevState) => ({ ...prevState, ...obj }));
const getData = () => {
console.log("getting data...");
setState({ searching: true });
fakeAPI().then((response) => {
console.log("response: ", response);
setState({ searching: false, data: response });
});
};
React.useEffect(() => {
if (debounceSearchTerm) {
getData();
}
}, [debounceSearchTerm]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
</div>
);
}
Let me know if you have more queries on this 😇
So it i think that list should be loaded a then you can filtering your loaded data.In your example on the beginning you don't have value so list is empty, you tape in some text and then value of list re render but it look like is not filtered.....
However I look through code base, and it's look like is not ready until you don't set manually open prop drop down list component. In getDerivedStateFromprops, next data list is read only if in next props is open set. to true
From DropDwonList
static getDerivedStateFromProps(nextProps, prevState) {
let {
open,
value,
data,
messages,
searchTerm,
filter,
minLength,
caseSensitive,
} = nextProps
const { focusedItem } = prevState
const accessors = getAccessors(nextProps)
const valueChanged = value !== prevState.lastValue
let initialIdx = valueChanged && accessors.indexOf(data, value)
//-->> --- -- --- -- -- -- -- - - - - - - - - - --- - - --------
//-->>
if (open)
data = Filter.filter(data, {
filter,
searchTerm,
minLength,
caseSensitive,
textField: accessors.text,
})
const list = reduceToListState(data, prevState.list, { nextProps })
const selectedItem = data[initialIdx]
const nextFocusedItem = ~data.indexOf(focusedItem) ? focusedItem : data[0]
return {
data,
list,
accessors,
lastValue: value,
messages: getMessages(messages),
selectedItem: valueChanged
? list.nextEnabled(selectedItem)
: prevState.selectedItem,
focusedItem:
(valueChanged || focusedItem === undefined)
? list.nextEnabled(selectedItem !== undefined ? selectedItem : nextFocusedItem)
: nextFocusedItem,
}
}
I would try:
<DropDownList
data={data}
filter
open
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
if it will be works, then you just have to
manage your open state by yourself.
I have a function I calling it in render() method
and it's setState a Flag from the state.
So I got this error
Cannot update during an existing state transition (such as within
render).
I read about this error, and what I understand it's because I setState in render method and this is the wrong way.
So I'm forced to do it if u have any idea to handle this tell me.
The main idea about this function
I have an array of an object "Name as Tools" so in every object I have "id, name, count, price"
so that will render a three input in UI like this
and I have a boolean flag in-state "isEmpty" that checks every input in array before sending this data to the database.
Code
State = {
toolsUsed: [
{
id: 0,
name: '',
price: '',
count: '',
},
],
// Checker
isEmpty: false,
}
renderToolsUsed = () => {
const {toolsUsed} = this.state;
const tools = toolsUsed.map((item, i) => {
const {count, price, name, id} = item;
this.setState({
isEmpty: ['name', 'price', 'count'].every(key => item[key].length > 0),
});
return (
<View key={i} style={styles.tools}>
.... Inputs here ...
</View>
);
});
return tools;
};
JSX
render() {
return (
<View>
{this.renderToolsUsed()}
</View>
);
}
You can't update the state like this. It is like infinite loop. setState will trigger render, then render will trigger another setState, then you keep repeat the circle.
I don't know why you need isEmpty when you already have toolsUsed which you can use it to check if all input are empty.
Lets say if you insist to have isEmpty, then you can set it inside input change event.
The code is not tesed. I wrote the code directly from browser. But you can get the idea before the code.
renderToolsUsed = () => {
const { toolsUsed } = this.state;
const tools = toolsUsed.map((item, i) => {
return (
<View key={i} style={styles.tools}>
<TextInput value={item.name} onChangeText={(text) => {
this.setState({
toolsUsed: [
...toolsUsed.slice(0, i - 1),
{...item, name: text },
...toolsUSed.slice(i)
]
}, this.updateEmptyState)
}>
// other input here
</View>
);
});
// ...
};
updateEmptyState = () => {
this.setState({
isEmpty: this.state.toolsUsed.every(x => x.name === '' && x.price === '' && x.count === '')
})
}
The state is not designed to store all the data you have in the app
For what you need isEmpty inside the state?
To do this, use a global variable
Or check it out when you want it out of the render
Scenario::
I have a parent component () that is using context API this is the Provider that passes the state to children components. Each of these children components has children ( a form to handle input ) when those forms are submitted I then use props to pass the input all the back up the parent component () aka the Provider whose job is to call an external script to calculate data from what the user submits which is received as a promise. Once I get the data I setState with the updated data.
My Problem::
I have two arrays of objects that are added to the state after the user submits the form. One containing all the values of the input the user typed in. Two the results the external script returned based on however many values the user typed in. Now is when I want to render both arrays to the view once we get them. Array one renders fine, but array two renders nothing. When I log array two in the console the array is filled, but if I log the array[index] and give it a specific index I get undefined
1st User submits the form
<KeywordSearch submitForm={this.handleKeywordFormSubmit} name="Bracket" />
Form is passed up to the Parent Component the Provider it looks like this
handleKeywordFormSubmit = () => {
let query = {
options: {
keyword: true
},
data: {
keyword_bracket: this.state.values
}
}
this.props.updateReport(query)
}
Values array is structured like this
values: [
{
value: 'input1',
label: 'input1'
},
{
value: 'input2',
label: 'input2'
}
]
2nd the parent component takes the query and runs the external script, returns the result which is an object which is then pushed in an array. When we're done the new array is added to the Context.Provider state to be accessed by the children components Context.Consumer
handleReportUpdate = (query) => {
if(query.options.keyword){
console.log('Keyword');
let keyword_bracket = []
query.data.keyword_bracket.forEach(bracket => {
tableBuilder.keywords(
this.state.thisReport.client.data.organic_research,
bracket
)
.then(keyword_summary => {
keyword_bracket.push(keyword_summary)
})
})
console.log(keyword_bracket)
let updatedReport = {
client: {
data: {
...this.state.thisReport.client.data,
keywordBrackets: keyword_bracket,
},
info: {
...this.state.thisReport.client.info,
Keyword: query.data.keyword_bracket
}
},
competitors: [...this.state.thisReport.competitors],
name: this.state.thisReport.name,
_id: this.state.thisReport._id
}
this.setState({
thisReport: updatedReport
})
}
}
3rd the Rendering stage in the child component the Context.Consumer
<Grid item xs={12} style={{margin: '20px 0'}}>
{
context.thisReport.client.data.keywordBrackets.length !== 0 ?
context.thisReport.client.data.keywordBrackets.map(bracket =>
{
return(
<div key={bracket.searchTerms.value}>{bracket.searchTerms.value}</div>
)
}
)
:
(<div>Nothing Here</div>)
}
</Grid>
<Grid item xs={12} style={{margin: '20px 0'}}>
<KeywordSearch submitForm={this.handleCompetitorFormSubmit}
name='Competitor' />
</Grid>
<Grid item xs={12} style={{margin: '20px 0'}}>
{
context.thisReport.client.info.Keyword.length !== 0 ?
context.thisReport.client.info.Keyword.map(bracket =>
{
return(
<div key={bracket.value}>{bracket.value}</div>
)
}
)
:
undefined
}
</Grid>
Here's where it's confusing because by following the process above when its time to render the new state from Context the codes second rendering that maps the context.thisReport.client.info.Keyword are rendered perfectly fine on the screen. The first rendering context.thisReport.client.data.keywordBrackets returns nothing. As a test, you can see I have added a <div>Nothing Here</div>
if the condition returns false. At first, before the user goes through the process of submitting the form that is shown on the screen as expected. Once they submit the form it disappears and where the return(
<div key={bracket.searchTerms.value}>{bracket.searchTerms.value}</div>) the output should be shown its blank. I have log's in the console that says the state is there the react dev tools confirms it as well. One weird thing is if I try to access the array by index I get undefined console.log(context.thisReport.client.data.keywordBrackets[0]) //undefined
This is a lot to take in so thanks in advance for reading. If you have any solutions advice lmk!!
Can you try the following?
handleReportUpdate = (query) => {
//create array of jobs
var getKeywordSummaries = [];
if(query.options.keyword){
console.log('Keyword');
let keyword_bracket = []
query.data.keyword_bracket.forEach(bracket => {
let getKeywordSummary = tableBuilder.keywords(
this.state.thisReport.client.data.organic_research,
bracket
)
.then(keyword_summary => {
keyword_bracket.push(keyword_summary)
})
//push each job into the jobs array
getKeywordSummaries.push(getKeywordSummary);
});
//wait until all jobs are done
Promise.all(getKeywordSummaries).then(function(results) {
console.log(keyword_bracket)
let updatedReport = {
client: {
data: {
...this.state.thisReport.client.data,
keywordBrackets: keyword_bracket,
},
info: {
...this.state.thisReport.client.info,
Keyword: query.data.keyword_bracket
}
},
competitors: [...this.state.thisReport.competitors],
name: this.state.thisReport.name,
_id: this.state.thisReport._id
}
this.setState({
thisReport: updatedReport
});
}
}
}