I am building a component that will be used for step-through processes such as :
This Workflow component takes an array of 'steps' as a prop and then it does the rest. Here is how it is being called in the image above :
let steps = [
{
display: "Sign Up Form",
component: SignupForm
},
{
display: "Verify Phone",
component: VerifyPhone
},
{
display: "Use Case Survey",
component: UseCase
},
{
display: "User Profile",
component: UserProfile
},
];
return (
<Workflow
steps={steps}
/>
);
The component field points to the component to be rendered in that step. For example the SignupForm component looks like this :
export default class SignupForm extends React.Component {
...
render() {
return (
<div>
<div className="page-header">
<h1>New User Sign Up Form</h1>
<p>Something here...</p>
</div>
<div className="form-group">
<input type="email" className="form-control" placeholder="Email address..." />
<small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
</div>
);
}
}
The issue I'm facing is that in each step there needs to be a Next button to validate the information in that step and move to the next. I was going to just put that button inside the component of each step, but that makes it less user-friendly. When a user clicks 'Next', and everything is valid, that step should be collapsed and the next step should open up. However this means that my Workflow component needs to render this button.
So, I need my Workflow component to call the method of each step component to validate the information in the step and return a promise letting it know if it passed or failed (with any error message). How do I need to call this method? Here is where the Workflow component renders all the steps
as <step.component {...this.props} />:
{
this.state.steps.map((step, key) => {
return (
...
<Collapse isOpen={!step.collapsed}>
<step.component {...this.props} />
<Button color="primary"
onClick={() => this.validate(key)}>Next</Button>
<div className="invalid-value">
{step.error}
</div>
</Collapse>
...
);
})
}
That renders the next button, as well as the onClick handler validate():
validate(i) {
let steps = _.cloneDeep(this.state.steps);
let step = steps[i];
step.component.handleNext().then(function () {
...
}).catch((err) => {
...
});
}
Ideally, step.component.validate() would call the validate method inside that component that has already been rendered:
export default class SignupForm extends React.Component {
....
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
.. which would have access to the state of that component. But, thats not how it works. How can I get this to work? I read a little about forwarding refs, but not exactly sure how that works. Any help is greatly appreciated!
One approach is to apply the Observer pattern by making your form a Context Provider and making it provide a "register" function for registering Consumers. Your consumers would be each of the XXXForm components. They would all implement the same validate API, so the wrapping form could assume it could call validate on any of its registered components.
It could look something like the following:
const WorkflowContext = React.createContext({
deregisterForm() {},
registerForm() {},
});
export default class Workflow extends React.Component {
constructor(props) {
super(props);
this.state = {
forms: [],
};
}
deregisterForm = (form) => {
this.setState({
forms: this.state.forms.slice().splice(
this.state.forms.indexOf(forms), 1)
});
};
registerForm = (form) => {
this.setState({ forms: [ ...this.state.forms, form ] })
};
validate = () => {
const validationPromises = this.state.forms.reduce(
(promises, formComponent) => [...promises, formComponent.validate()]);
Promise.all(validationPromises)
.then(() => {
// All validation Promises resolved, now do some work.
})
.catch(() => {
// Some validation Promises rejected. Handle error.
});
};
render() {
return (
<WorkflowContext.Provider
value={{
deregisterForm: this.deregisterForm,
registerForm: this.registerForm,
}}>
{/* Render all of the steps like in your pasted code */}
<button onClick={this.validate}>Next</button
</WorkflowContext.Provider>
);
}
}
// Higher-order component for giving access to the Workflow's context
export function withWorkflow(Component) {
return function ManagedForm(props) {
return (
<WorkflowContext.Consumer>
{options =>
<Component
{...props}
deregisterForm={options.deregisterForm}
registerForm={options.registerForm}
/>
}
</WorkflowContext.Consumer>
);
}
}
SignupForm and any other form that needs to implement validation:
import { withWorkflow } from './Workflow';
class SignupForm extends React.Component {
componentDidMount() {
this.props.registerForm(this);
}
componentWillUnmount() {
this.props.deregisterForm(this);
}
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
// Register each of your forms with the Workflow by using the
// higher-order component created above.
export default withWorkflow(SignupForm);
This pattern I originally found applied to React when reading react-form's source, and it works nicely.
Related
First timer here on StackOverflow! I'm trying to emulate a terminal interface for one of my portfolio projects. The way I have envisioned the terminal is that each terminal box has a state object with a few key/value pairs. Ideally, when someone enters text into the terminal box input form, the input becomes disabled and a new terminal box is rendered on the screen with a dynamic response based upon the userInput text which has been saved in the state. Where I'm stuck:
Once userInput state has been updated, how do I get a new terminal box to render beneath the prior box on the screen?
Prior to rendering, how do I set the initial state of the newly-rendered terminal box back to default with the exception of the "output" which would be re-valued to an appropriate response that I set?
How do I access the state in the prior terminal box so I can "read" the userInput stored there so I can determine what the appropriate response to that input would be?
I've included copies of each of the components below:
App.js
import React from "react";
import Terminal from "./components/Terminal";
import "./App.css";
class App extends React.Component {
render() {
return (
<div>
<Terminal />
</div>
);
}
}
export default App;
Terminal.js
import React, { Component } from "react";
import Form from "./Form";
import Falcon from "./Falcon";
import Messages from "./Alerts/Messages";
class Terminal extends Component {
state = {
output: Messages.intro,
userInput: "",
isComplete: false,
isDisabled: "",
};
markComplete = () => {
this.setState({
isComplete: true,
});
};
onSubmit = (event, userInput) => {
event.preventDefault();
this.setState({
userInput: userInput,
isDisabled: "disabled",
});
};
render() {
return (
<div>
<Falcon
output={this.state.output}
markComplete={this.markComplete}
isComplete={this.state.isComplete}
/>
<p />
<Form
input={this.state.userInput}
onSubmit={this.onSubmit}
isComplete={this.state.isComplete}
isDisabled={this.state.isDisabled}
/>
<p />
</div>
);
}
}
export default Terminal;
Falcon.js (Note: You'll see that there is a component "Typed" below - that is part of Matt Boldt's Typed.js (of which react-typed is an offshoot package) package which I'm using to simulate typing.)
import React, { Component } from 'react'
import Typed from 'react-typed'
class Falcon extends Component {
state = {
output: this.props.output,
};
render() {
return (
<div>
<Typed
strings={[this.state.output]}
typeSpeed={40}
onComplete={(self) => {
self.cursor.remove();
this.props.markComplete();
}}
/>
</div>
);
}
}
export default Falcon;
Form.js
import React from "react";
class Form extends React.Component {
state = {
input: this.props.input,
};
render() {
return (
<form
style={{
display: this.props.isComplete === false ? "none" : "",
}}
onSubmit={(event) => {
this.props.onSubmit(event, this.state.input);
}}
>
{"> "}
<input
ref={(input) => input && input.focus()}
type="text"
disabled={this.props.isDisabled}
style={{
border: "none",
outline: "none",
backgroundColor: "#FFF",
color: "#000",
}}
value={this.state.input}
onChange={(event) => this.setState({ input: event.target.value })}
/>
</form>
);
}
}
export default Form;
Any insight or guidance you can offer would be much appreciated! Thank you for helping this "first-timer" out!
Welcome to StackOverflow! I made a codesandbox demo with a few changes.
When developing React applications, it's a good practice to model the UI (the DOM elements) as a function of your internal state. You update the state and the UI changes automatically, it reacts to updates.
That said, you probably want to consider using the form only for the actual input element at the bottom of the terminal. The "past buffer" is just an array that only increases its content with user input and program output. Another good practice (actually a commandment!) is to never mutate the state. So, if you want to update the array, you create a new one from scratch, as in:
this.setState((state) => ({
conversation: [
...state.conversation, // we spread the previous state into the new one
{ text: state.userInput, id: faker.random.uuid(), type: "input" } // the last element is appended
]
}));}
Notice how setState (in class components) just schedules an update for the fields that you used. As your app scales, you will probably want to limit the length of this array.
The terminal component could be like:
class Terminal extends Component {
state = {
output: "Messages.intro",
userInput: "",
isComplete: false,
isDisabled: "",
conversation: [] // couldn't think of a nice name :(
};
markComplete = () => {
this.setState({
isComplete: true
});
};
onChange = (event) => {
this.setState({ userInput: event.target.value });
};
onSubmit = (event) => {
event.preventDefault();
this.setState((state) => ({
userInput: "",
conversation: [
...state.conversation,
{ text: state.userInput, id: faker.random.uuid(), type: "input" }
]
}));
};
render() {
const { conversation, userInput, output, isComplete } = this.state;
return (
<div>
<Falcon
output={output}
markComplete={this.markComplete}
isComplete={isComplete}
/>
<p />
// This is not really a form. Should be changed to another readonly element
{conversation.map((item, index) => (
<Form
key={item.id}
input={item.text}
onSubmit={this.onSubmit}
isComplete={isComplete}
isDisabled
/>
))}
<p />
<Form
input={userInput}
onSubmit={this.onSubmit}
isComplete={isComplete}
isDisabled={false}
onChange={this.onChange}
/>
<p />
</div>
);
}
}
In component A ,I've a btn with search filters , After clicking btn im sending the selected values from search filters into component B where it is getting data from api and rendering the info by using table.
So the problem im facing is, At 1st time btn click all selected values are passed to component B and also data is rendering but when im clicking the btn for 2nd time after changing search filter values , the btn func is not working. I've also checked in console by displaying logs in component B. Im able to see only 1st call logs but not 2nd call.
Im new to React & also rarely develop front-end. Can anyone tell where am i doing mistake ?
Component A:
class Hook extends React.Component<any ,any > {
constructor(props) {
super(props);
this._onButtonClick = this._onButtonClick.bind(this);
this.state = {
showComponent: false,
};
};
_onButtonClick() {
this.setState({
showComponent: true,
});
}
render() {
const {classes} = this.props;
return (
<div>
<Box border={1} className={classes.root} display="flex" >
<div>
<Button variant="contained" color="primary"
onClick={this._onButtonClick}>
Search
</Button>
</div>
</Box>
{this.state.showComponent ?
<BComponent a = {this.state.dropdown1value} b = {this.state.dropdown2value} c = {this.state.dropdown3value} d = {this.state.dropdown4value}/>
: null }
</div>
);
}
}
Component B:
class BComponent extends React.Component<any ,any > {
constructor(props) {
super(props);
this.getAPIInfo = this.getAPIInfo.bind(this);
this.state = {
result: { allInfo: [] },
};
}
componentDidMount() {
this.getAPIInfo(this.props.a,this.props.b,this.props.c,this.props.d).then(Response => {this.setState({result: Response })});
this.setState({loaderFlag: true});
}
async getAPIInfo(a, b, c, d) {
let res;
try {
calling API
});
} catch (error) {
console.log("API call error", error);
} this.setState({loaderFlag: false});
return res;
}
render() {
const {classes} = this.props;
if(this.state.loaderFlag) {
return (
<Loader /> );
}
else
return (
Rendering API data through material UI table
);
}
}
export default withStyles(useStyles)(BComponent)
Ideally the ComponentB should be a dumb/presentation component and Component A should fetch the data and send it over to ComponentB as props.
this._onButtonClick
should then fetch the data from API and pass it over to componentB for display.
I am learning reactjs and I wrote component with the method componentWillReceiveProps (cWRP) but I read that it is deprecated and it must replace with getDerivedStateFromProps (gDSFP) - https://en.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html.
Please note that the following code has the sole purpose of illustrating my problem and questions. It is not a full code.
App.js file :
import React from 'react';
import './App.css';
import Display from './component.js'
class App extends React.Component {
state={resetCounter:false}
resetCounter= () => this.setState( {resetCounter: true} );
render() {
return (
<div className="App">
<header className="App-header">
<Display resetCounter={this.state.resetCounter}></Display>
<div>
<p></p><p></p>
<button onClick={this.resetCounter}>Reset</button>
</div>
</header>
</div>
);
}
componentDidUpdate () {
if (this.state.resetCounter!==false)
this.setState( {resetCounter: false} );
}
}
export default App;
component.js file
import React from 'react'
class Display extends React.Component {
constructor() {
super();
this.state = this.resetState();
this.state.generalCounter=0;
}
/* method to avoid code duplication in constructor and cWRP
could not be used with getDerivedStateFromProps */
resetState = () => ({resettableCounter: 0,});
componentWillReceiveProps(nextProps) {
if (nextProps.resetCounter===true)
this.setState(this.resetState())
}
render() {
return (
<>
<div>
<div>general counter : {this.state.generalCounter}</div>
<div>resettable counter : {this.state.resettableCounter}</div>
</div>
<div>
<button onClick={this.incCounters}>+</button>
<button onClick={this.decCounters}>-</button>
</div>
</>
)
}
incCounters= () => this.setState(
{
resettableCounter: this.state.resettableCounter+1,
generalCounter: this.state.generalCounter+1
}
)
decCounters= () => this.setState(
{
resettableCounter: this.state.resettableCounter-1,
generalCounter: this.state.generalCounter-1
}
)
}
export default Display
In the state of the component, there is a resettable part and a non resettable one. A method resetState is used to avoid code duplication in the constructor and in cWRP.
To replace cWRP by gDSFP, I wrote a class method because instance method could NOT be called in gDSFP (this is not usable)
...
constructor() {
super();
this.state = Display.resetState();
this.state.generalCounter=0;
}
static resetState () {
return ({resettableCounter: 0,});
}
static getDerivedStateFromProps(nextProps) {
if (nextProps.resetCounter === true) {
return Display.resetState();
} else {
return null;
}
}
...
With this solution, it is very easy to modify all my components but I am not sure that it is a good mean.
I wonder if I have a misconception and if I should rewrite my components to separate them into Fully controlled components and Fully uncontrolled components with a key ( https://en.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#preferred-solutions).
For example, in this case, do I have to write :
One Fully uncontrolled components for the resettable counter
One Fully controlled one for the non resettable counter
A parent component with the +/- buttons to render them.
I ask this question because in some cases, it will be much work, so I want to be sure before continuing.
You would want to keep the gdsfp version in your post if your component depends on some outside props, which you don't have controll over (such as JSON returned or 3rd party render props component, etc).
It looks like you have a full control over what's passed down to the Display. You can pass down an initial resettableCounter value down to Display.
The advantage is two-folds.
Your Display props shows what the Display does - Making it more descriptivie/readable.
It's easier to maintain, as you don't have to massage the data.
For your particular case, Fully uncontrolled component with a key seems to make more sense, as Display should accept the initial value to show, but is responsible for managing the reseetableCounter.
Unless it's absolutely unavoidable, don't create components which control their siblings (or parents). Instead, lift state to a common ancestor:
const Display = ({
generalCounter,
resettableCounter,
incrementCounters,
decrementCounters,
}) => (
<div>
<div>General Counter: {generalCounter}</div>
<div>Resettable Counter: {resettableCounter}</div>
<button onClick={incrementCounters}>Increment</button>
<button onClick={decrementCounters}>Decrement</button>
</div>
);
class DisplayContainer extends React.Component {
state = {
generalCounter: 0,
resettableCounter: 0,
};
incrementCounters = () => this.setState(prevState => ({
generalCounter: prevState.generalCounter + 1,
resettableCounter: prevState.resettableCounter + 1,
}));
decrementCounters = () => this.setState(prevState => ({
generalCounter: prevState.generalCounter - 1,
resettableCounter: prevState.resettableCounter - 1,
}));
resetResettableCounter = () => this.setState({
resettableCounter: 0,
});
render() {
return (
<React.Fragment>
<Display
{...this.state}
incrementCounters={this.incrementCounters}
decrementCounters={this.decrementCounters}
/>
<button onClick={this.resetResettableCounter}>
Reset Resettable Counter
</button>
</React.Fragment>
);
}
}
const App = () => (
<div>
<DisplayContainer />
</div>
);
An alternative approach would be something like Redux (which effectively lifts state out of React).
I've have an component "A" with a button. When the user press the button I'm showing a modal(react-responsive-modal) with bunch of filed and an update button. When the user presses the update button on the modal I want to reload the component "A" with the updated data.
I tried redirecting using this.props.history.push('dashboard/componentA') but it didn't work. Then I tried redirecting to the dashboard and again redirecting to the component like this
this.props.history.push('/dashboard');
this.props.history.push('/dashboard/componentA');
It worked but I'm not seeing any loader that I've used on 'componentWillMount' and the component just freezes up. I couldn't scroll up or down.
Try not to use the browser history as a way to update react (as much as you can). React is designed to re-render components when the props or state for that component change. As an example, this should trigger an update in ComponentA without needing to update the browser's history:
class ComponentA extends Component {
handleModalClick = (event) => {
this.setState({
componentData: event.data,
});
}
render() {
return (
<ReactModal onClick={this.handleClick} />
)
}
}
EDIT: Updated to show a data fetching parent component:
class DataFetcher extends Component {
saveAndFetchData = (saveData) => {
FetchDataPromise(saveData).then((updatedData) => {
this.setState({ data: updatedData });
}
}
render() {
const { data } = this.state;
return (
<div>
<ComponentA data={data} />
<ReactModalComponent handleClick={saveAndFetchData} />
</div>
);
}
}
class ComponentA extends Component {
render() {
const { data } = this.props;
return (
<div>
...render data...
</div>
)
}
}
I am trying to build a multi-stage form in React. I build a simple demo to try out the functionality of going through the form. As a template, I used this tutorial I found on the web.
The error message I am getting is
Warning: Cannot update during an existing state transition (such as within "render"). Render methods should be a pure function of props and state.
after I click on the continue button.
I looked through multiple StackOverflow questions, which had the same error message, but none helped me to resolve my issue.
MainForm
export class MainForm extends Component {
state = {
step: 1
};
nextStep = () => {
const {step} = this.state;
this.setState({
step: step + 1
});
};
prevStep = () => {
const {step} = this.state;
this.setState({
step: step - 1
});
};
render() {
const {step} = this.state;
switch (step) {
case 1:
return (
<PatientDetails
nextStep={this.nextStep}
/>
);
case 2:
return(
<Summary
prevStep={this.prevStep()}
/>
);
default:
return(
<h1>Something went wrong, kiddo</h1>
);
}
}
}
export default MainForm
PatientDetails
export class PatientDetails extends Component {
continue = e => {
e.preventDefault();
this.props.nextStep();
};
render() {
return (
<Form horizontal>
<Button bsStyle="primary" onClick={this.continue}>Continue</Button>
</Form>
);
}
}
export default PatientDetails;
Summary
export class Summary extends Component {
back = e => {
e.preventDefault();
this.props.prevStep();
};
render() {
return (
<Form horizontal>
<Button bsStyle="primary" onClick={this.back}>Continue</Button>
</Form>
);
}
}
export default Summary;
As described in the comments, the warning indicates that setState is (unintentionally) being called in the middle of the render call.
Looking at your code the culprit is to be found for when you attempt to render the Summary component as the handler you pass to it, is accidentally being called since you wrote prevStep={this.prevStep()}. By removing the parentheses and writing prevStep={this.prevStep} your issue should be fixed.