I have a REACT app which is basically a till for adding items to an order. I have my OrderSection which does most of the grunt work, including having a barcode scanner, and I have my Search component which is a child of OrderSection, and if someone clicks on a search result it passes that back up to OrderSection via a prop callback.
Now, this is what I initially had, but it had problems:
#autobind
class OrderSection extends React.Component {
constructor(props) {
super(props);
this.state = {
orderItems: [],
multiMatch: [],
};
}
async barcodeScanner(barcode) {
let response;
try {
response = await serverApi.getItemsFromBarcode(barcode);
} catch(e) {
return toast.error(e.message || e.toString());
}
let {items} = response;
if (items.length === 0) {
toast.info('no matching items found');
} else if (items.length === 1) {
this.addItem(items[0]);
} else {
// show results in the 'search' section
this.setState({multiMatch: items})
}
}
addItem(item) {
// doesn't really matter what happens here
}
async lookupAdd(no, code) {
try {
let {items} = await serverApi.getItems(no, code);
let item = items[0];
if (item) {
this.addItem(item);
} else {
}
} catch(e) {
toast.error(e.toString());
}
}
render() {
return (
<section>
// render items up here
<Search
onItemClick={this.lookupAdd}
results={this.state.multiMatch} />
</section>
)
}
}
#autobind
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
show: false // change to true to show the search
}
}
// code for updating search results on form submit
// updating this.state.searchResults
render() {
return (
<React.Fragment>
// form with search text input here
// render searchResults here
</React.Fragment>
)
}
componentWillReceiveProps(props) {
if (props.results.length) {
this.setState({searchResults: props.results, show: true});
}
}
}
Search.propTypes = {
onItemClick: PropTypes.func.isRequired,
results: PropTypes.array
};
The main issue here is how in OrderSection, in barcodeScanner, when I have multiple matches, I pass them down as a prop into Search, and then Search sees that prop and updates itself in the componentWillReceiveProps function.
I wasn't entirely happy with what was happening there -- it was actually fine most of the time, but there was some annoying unexpected behaviour of Search showing itself when the prop actually hadn't changed.
So I came up with the idea of passing a callback up from Search to OrderSection:
#autobind
class OrderSection extends React.Component {
constructor(props) {
super(props);
this.state = {
orderItems: []
};
}
async barcodeScanner(barcode) {
let response;
try {
response = await serverApi.getItemsFromBarcode(barcode);
} catch(e) {
return toast.error(e.message || e.toString());
}
let {items} = response;
if (items.length === 0) {
toast.info('no matching items found');
} else if (items.length === 1) {
this.addItem(items[0]);
} else {
// show results in the 'search' section
this.sendMultiMatchToSearch(items);
}
}
setSearchResultsFunc(func) {
this.sendMultiMatchToSearch = func;
}
addItem(item) {
// doesn't really matter what happens here
}
async lookupAdd(no, code) {
try {
let {items} = await serverApi.getItems(no, code);
let item = items[0];
if (item) {
this.addItem(item);
} else {
}
} catch(e) {
toast.error(e.toString());
}
}
render() {
return (
<section>
// render items up here
<Search
onItemClick={this.lookupAdd}
manuallySetResultsFunc={this.setSearchResultsFunc}
/>
</section>
)
}
}
#autobind
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
show: false // change to true to show the search
};
if (typeof this.props.manuallySetResultsFunc === "function") {
const func = (results) => {
this.setState({searchResults: results, show: true});
this.flash();
};
this.props.manuallySetResultsFunc(func);
}
}
render() {
return (
<React.Fragment>
// render searchResults here
</React.Fragment>
)
}
}
Search.propTypes = {
onItemClick: PropTypes.func.isRequired,
manuallySetResultsFunc: PropTypes.func
};
But I feel like this is probably bad react practice. It's producing the behavior I want but I think if a React expert looks at this they wouldn't like it.
Can I get some advice on the proper way to pass search results down to Search to trigger it, while still otherwise allowing the SEARCH element to control its own searchResults code
You're right in that you shouldn't have to 'intervene' in this way to modify how your state is updated. You should just set up your state and props and then things should take care of themselves.
Here are some straightforward approaches that I'd typically use:
1) From the OrderSection parent to conditionally render your Search only when there are items:
render() {
return (
<section>
{this.state.multiMatch && <Search
onItemClick={this.lookupAdd}
manuallySetResultsFunc={this.setSearchResultsFunc}
/>}
</section>
)
}
2) Within the <Search> child:
render() {
return (
<React.Fragment>
{this.state.searchResults && this.state.searchResults.map(result=> // map to SearchResults)}
</React.Fragment>
)
}
3) From the OrderSection parent pass in 'isShowing' as a prop:
render() {
const isShowing = !!this.state.multiMatch; // add other logic here if necessary
return (
<section>
<Search
onItemClick={this.lookupAdd}
isShowing={isShowing}
/>
</section>
)
}
Then in your Search, extract isShowing from props.
The idea is that you only need to update the state and the rendering should take care of itself.
I would introduce additional props to Search component showMultiMatch and onSearchClose and add showSearch to OrderSection component(which is set to true when you receive multiMatch and set to false in the onSearchClose handler). Remove componentWillReceiveProps and check condition this.props.showMultiMatch || this.state.show in the render function to render search conditionally.
Related
Let's say I've a parent component A and a child B:
A:
class A {
constructor() {
this.state = {data: []};
}
handleClick = () => {
// api call
// set data state to the returned value from api
// call B's createTable method
}
render() {
return(
<div>
<button onClick={()=> this.handleClick()}>Fetch data</button>
<B data={this.state.data} />
</div>
}
}
B:
class B {
constructor() {
this.state = {...};
}
createTable = () => {
const { data } = this.props;
// do smth
}
render() {
return(...);
}
}
I want to call createTable method from A without using Refs.
What I've done so far is using componentDidUpdate life cycle method in B to check if data prop has changed or not, If it changed call createTable method but I want to know is this right? or there's a better way of doing it because I feel it is kinda hacky or maybe bad design.
class B {
constructor() {
this.state = {...};
}
componentDidUpdate(prevProps) {
const { data } = this.props;
if (data !== prevProps.data) {
this.createTable();
}
}
createTable = () => {
const { data } = this.props;
// do smth
}
render() {
return(...);
}
}
NOTE I don't want to use hooks either just class based component.
The following example might be useful
class Parent extends Component {
render() {
return (
<div>
<Child setClick={click => this.clickChild = click}/>
<button onClick={() => this.clickChild()}>Click</button>
</div>
);
}
}
class Child extends Component {
constructor(props) {
super(props);
this.getAlert = this.getAlert.bind(this);
}
componentDidMount() {
this.props.setClick(this.getAlert);
}
getAlert() {
alert('clicked');
}
render() {
return (
<h1 ref="hello">Hello</h1>
);
}
}
I have a function outside of render. That function returns (conditionally) a component, that function is beeing triggered not inside render, but inside componentWillReceiveProps (which was necessary due to other facts).
My problem is that the function does not end up returning the component and I dont know why. When I call that function inside render, then of it works, but I cant do that as I must call it inside componentWillReceiveProps. Any ideas? Thanks!!
class App extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.user != this.props.user) {
this.getData(nextProps.user)
}
}
getData() {
if (...) {
return <Child />
}
}
render() {
return (
<div>{this.getData}</div>
);
}
}
const Child = () => {
return <h1>Hello</h1>
}
Create a state called data in the constructor as follows:
constructor(props){
super(props);
this.state={data:""};
}
Now, {this.getdata} inside render() with {this.state.data}
Also replace componentWillReceiveProps as follows:
componentWillReceiveProps(nextProps) {
if (nextProps.user != this.props.user) {
var newdata = this.getData(nextProps.user)
this.setState({data:newdata});
}
}
Because you can't return children from other hooks than render you will need to keep them in a state:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
someChildren: null
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.user != this.props.user) {
this.setState({ someChildren: this.getData(nextProps.user) });
}
}
getData() {
if (...) {
return <Child />;
}
return null
}
render() {
return <div>{this.state.someChildren}</div>;
}
}
When you component will receive new props, it will re-render automatically, doing like following you should have you component to re-render and being updated:
class App extends React.Component {
getData: () => {
if (...) {
return <Child />
}
return null;
};
render() {
return (
<div>{this.getData()}</div>
);
}
}
componentWillReceiveProps is React lifecycle method which is invoked as soon as your React Component receive a prop by the parent. Actions that could be performed in there are for example update the state what you are doing instead is calling a getDate method which is returning a React Component .
A possible implementation could be:
class App extends React.Component {
getData() {
const { user } = this.props;
return user ? <Child /> : <div />
}
render() {
return (
<div>
{this.getData()}
</div>
)
}
}
const Child = () => {
return <h1>Hello</h1>
}
You can only return JSX data in render and not in the other lifecycle function to render. Also render method is pure so for the same input it returns the same output and hence react is able to rightly optimise on performance for the same by maintaining a virtual dom, so you would just write
class App extends React.Component {
getData() {
if (...) {
return <Child />
}
}
render() {
return (
<div>
{this.getData()}
</div>
)
}
}
const Child = () => {
return <h1>Hello</h1>
}
and it would have a the same effect, also if you further optimise by using React.PureComponent, so that render is called on when there is a prop change. React.PureComponent implements shouldComponentUpdate with a shallow prop and state comparison.
class App extends React.PureComponent {
componentWillReceiveProps(nextProps) {
if (nextProps.user != this.props.user) {
this.getData(nextProps.user)
}
}
getData() {
if (...) {
return <Child />
}
}
render() {
return (
<div>
{this.getData()}
</div>
)
}
}
However to do what you want, you would actually store the date in state of component and then render the data based on state in render method
class App extends React.Component {
constructor(props) {
super(props);
this.state {
data: this.getData(props)
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.user != this.props.user) {
this.getData(nextProps.user)
}
}
getData(props) {
if (...) {
const newData;
// update newData based on receivedProps here
// store the data in state
this.setState({data: newData});
}
return [];
}
render() {
return (
<div>
{this.state.data.map((obj) => return <Child data={obj}/>)}
</div>
)
}
}
I'm implementing search with pagination in React. So far I found few examples of it, but all they use code with double setState(), before and after AJAX call to backend. For example my current solution is:
import React from "react"
import PropTypes from "prop-types"
import SearchField from "components/SearchField"
import SearchResults from "components/SearchResults"
import Item from "models/Item"
class Search extends React.Component {
constructor() {
super()
this.state = {
query: "",
page: 1,
foundItems: []
}
this.handleSearch = this.handleSearch.bind(this)
this.handlePageChange = this.handlePageChange.bind(this)
}
updateSearchResults() {
const query = this.state.query
const params = {
page: this.state.page
}
Item.search(query, params).then((foundItems) => {
this.setState({ foundItems })
})
}
handleSearch(event) {
this.setState({
query: event.target.value
}, this.updateSearchResults)
}
handlePageChange(data) {
this.setState({
page: data.selected + 1
}, this.updateSearchResults)
}
render() {
return (
<div className="search">
<SearchField onSearch={this.handleSearch} />
<SearchResults
onPageChange={this.handlePageChange}
onSelect={this.props.onSelect}
items={this.state.foundItems}
/>
</div>
)
}
}
Search.propTypes = {
onSelect: PropTypes.func.isRequired
}
export default Search
I know that I can change interface of updateSearchResults to receive query and page as arguments and then I can avoid first setState to pass values there, but it doesn't look like a good solution, because when list of search parameters will grow (sorting order, page size, filters for example) then it'll get a bit clumsy. Plus I don't like idea of manual state pre-management in handleSearch and handlePageChange functions in this way. I'm looking for a better implementation.
I am not fully sure what you are asking, but you can optimise your code a bit by doing the following:
class Search extends React.Component {
constructor() {
super()
this.page = 1;
this.query = "";
this.state = {
foundItems: []
}
this.handlePageChange = this.handlePageChange.bind(this)
}
updateSearchResults(event) {
if(typeof event === "object")
this.query = event.target.value;
const params = {
page: this.page
}
Item.search(this.query, params).then((foundItems) => {
this.setState({ foundItems })
})
}
handlePageChange(data) {
this.page = data.selected + 1;
this.updateSearchResults();
}
render() {
return (
<div className="search">
<SearchField onSearch={this.updateSearchResults} />
<SearchResults
onPageChange={this.handlePageChange}
onSelect={this.props.onSelect}
items={this.state.foundItems}
/>
</div>
)
}
}
I am rewriting some old ReactJS code, and got stuck fixing this error (the error repeats about 1700 times in the console, the DOM does not render at all):
Warning: setState(...): Cannot update during an existing state
transition (such as within render or another component's
constructor). Render methods should be a pure function of props and
state; constructor side-effects are an anti-pattern, but can be moved
to componentWillMount.
I am a Component that passes it's state down to a component that should render some controls. Based on the clicked controls, the state should change, and new controls should render.
So this is my Container component:
class TeaTimer extends Component {
constructor(props) {
super(props);
this.state = {
count: 120,
countdownStatus: 'started'
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.countdownStatus !== prevState.countdownStatus) {
switch (this.state.countdownStatus) {
case 'started':
this.startTimer();
break;
case 'stopped':
this.setState({count:0});
}
}
}
componentWillUnmount() {
clearInterval(this.timer);
delete this.timer;
}
startTimer() {
this.timer = setInterval(() => {
let newCount = this.state.count -1;
this.setState({
count: newCount >= 0 ? newCount : 0
});
if(newCount === 0) {
this.setState({countdownStatus: 'stopped'});
}
}, 1000)
}
handleStatusChange(newStatus) {
this.setState({ countdownStatus: newStatus });
}
render() {
let {count, countdownStatus} = this.state;
let renderStartStop = () => {
if (countdownStatus !== 'stopped') {
return <StartStop countdownStatus={countdownStatus} onStatusChange={this.handleStatusChange()}/>
} else {
return <div>This will be the slider form</div>
}
};
return(
<div className={styles.container}>
<p>This is the TeaTimer component</p>
<Clock totalSeconds={count}/>
{renderStartStop()}
</div>
)
}
}
And this is my controls component:
class StartStop extends Component {
constructor(props) {
super(props);
}
onStatusChange(newStatus) {
return() => {
this.props.onStatusChange(newStatus);
}
}
render() {
let {countdownStatus} = this.props;
let renderStartStopButton = () => {
if(countdownStatus === 'started') {
return <button onClick={()=> this.onStatusChange('stopped')}>Reset</button>;
} else {
return <button onClick={()=> this.onStatusChange('started')}>Start</button>
}
};
return(
<div className={styles.tt.Controls}>
{renderStartStopButton()}
</div>
)
}
}
StartStop.propTypes = {
countdownStatus: React.PropTypes.string.isRequired,
onStatusChange: React.PropTypes.func.isRequired
};
I am sorry about the wall of text, but I really can;t figure out where the error is coming from - and therefor don't know which part of the code I can leave out.
I have tried implementing the solution found in a seemingly related question, but can't get it to work either.
I think you have a typo in this line:
return <StartStop countdownStatus={countdownStatus} onStatusChange={this.handleStatusChange()}/>
It should be:
return <StartStop countdownStatus={countdownStatus} onStatusChange={() => this.handleStatusChange}/>
You seem to be calling the method handleStatusChange instead of passing it as a callback.
Your metods call each other so you must define two instance of your metods.
class StartStop extends Component {
constructor(props) {
super(props);
this.onStatusChangeReset=this.onStatusChange.bind(this);
this.onStatusChangeStart=this.onStatusChange.bind(this);
}
onStatusChange(newStatus) {
return() => {
this.props.onStatusChange(newStatus);
}
}
render() {
let {countdownStatus} = this.props;
let renderStartStopButton = () => {
if(countdownStatus === 'started') {
return <button onClick={this.onStatusChangeReset('stopped')}>Reset</button>;
} else {
return <button onClick={this.onStatusChangeStart('started')}>Start</button>
}
};
return(
<div className={styles.tt.Controls}>
{renderStartStopButton()}
</div>
)
}
}
StartStop.propTypes = {
countdownStatus: React.PropTypes.string.isRequired,
onStatusChange: React.PropTypes.func.isRequired
};
In this line in your return <StartStop countdownStatus={countdownStatus} onStatusChange={this.handleStatusChange()}/> gives the warning, the handleStatusChanged function is called on pressing a button which tries to change the state by setState keyword. whenever the state is changed render function is called again but in your case render function was in progress of returning while the render function is called again by setState keyword.
I am waiting the props to come up from a store named GetDealersStore, and the way I am fetching that data is with an action where I am doing this:
componentWillMount () { GetDealersActions.getDealers(); }
I already test the app and componentWillMount() is running before the initial render where I have this
let dealerInfo;
if (this.state.dealerData) {
dealerInfo = this.state.dealerData.dealersData.map((dealer) => {
return (<div>CONTENT</div>);
})
} else {
dealerInfo = <p>Loading . . .</p>
}
but for the first second you can see <p>Loading . . .</p> in the screen which is the else in the conditional above, and then the rest of the render comes up with return (<div>CONTENT</div>); which is the if in the conditional. So, I guess, this means that the render method has been trigger twice because it keeps waiting for the data coming from the database.
The data from the database is not available at the time of the 1st render, so, how can I fetch that data before the 1st initial render occurs?
You can't do this with a single component. You should follow the Container Component pattern to separate data from rendering.
let DealersContainer = React.createClass({
getInitialState() {
return {dealersData: []};
},
componentWillMount() {
GetDealersActions.getDealers();
},
render() {
let {dealersData} = this.state;
return (<div>
{dealersData.map((dealer) => {
let props = dealer;
return (<Dealer ...props />); // pass in dealerData as PROPS here
})}
</div>);
}
});
Then update your Dealer component to receive props and render the actual content.
My answer is similar to Mathletics', just in more detail.
In this example I've included initialising state of dealerData to null; this is the check that's made to determine whether the data has been returned from the store by the container.
It's verbose, but declarative, and does what you want, in the order that you want, and it will work each time.
const DealerStore = MyDataPersistenceLibrary.createStore({
getInitialState() {
return {
dealerData: null
};
},
getDealers() {
// some action that sets the dealerData to an array
}
});
const DealerInfoContainer = React.createClass({
componentWillMount() {
DealerStoreActions.getDealers();
},
_renderDealerInfo() {
return (
<DealerInfo {...this.state} />
);
},
_renderLoader() {
return (
<p>Loading...</p>
);
},
render() {
const { dealerData } = this.state;
return (
dealerData
? this._renderDealerInfo()
: this._renderLoader()
);
}
});
const DealerInfo = React.createClass({
getDefaultProps() {
return {
dealerData: []
};
},
_renderDealers() {
const { dealerData } = this.props;
return dealerData.map(({ name }, index) => <div key={index}>{name}</div>);
},
_renderNoneFound() {
return (
<p>No results to show!</p>
);
},
render() {
const { dealerData } = this.props;
return (
dealerData.length
? this._renderDealers()
: this._renderNoneFound()
);
}
});