I implemented the componentShouldUpdate in the code below to try and speed up performance. This goal was accomplished, but now the comments don't render. Browser console shows that everything is being received, though. There's also a div that renders the number of comments and that is being updated as well.
class ProposalDetail extends React.Component {
constructor(props) {
super(props);
this.state = {
sortedComments: []
};
}
componentDidUpdate(prevProps) {
if((!prevProps.proposal || Object.keys(prevProps.proposal).length === 0 ) &&
this.props.proposal && Object.keys(this.props.proposal).length > 0 &&
this.props.proposal.status === 4 ){
prevProps.onFetchProposalVoteStatus(prevProps.token);
}
this.handleUpdateOfComments(prevProps, this.props);
}
shouldComponentUpdate(nextProps, nextState) {
console.log('thisProps', this.props.comments)
console.log('nextProps', nextProps.comments)
if (this.props.comments === nextProps.comments) {
return true
}
else {
return false
}
}
componentDidMount() {
this.props.onFetchLikedComments(this.props.token);
}
componentWillUnmount() {
this.props.resetLastSubmittedProposal();
}
handleUpdateOfComments = (currentProps, nextProps) => {
let sortedComments;
if(!nextProps.comments || nextProps.comments.length === 0) {
return;
}
// sort option changed
if(currentProps.commentsSortOption !== nextProps.commentsSortOption) {
sortedComments = updateSortedComments(
this.state.sortedComments,
nextProps.commentsSortOption
);
}
// new comment added
if(currentProps.comments.length !== nextProps.comments.length) {
const isEmpty = currentProps.comments.length === 0;
const newComments = isEmpty ?
nextProps.comments :
[nextProps.comments[nextProps.comments.length - 1]]
.concat(this.state.sortedComments);
sortedComments = updateSortedComments(
newComments,
currentProps.commentsSortOption,
nextProps.commentsvotes,
isEmpty
);
}
// usernames aren't fully merged into comments
const commentWithoutAnUsername = comments => comments.filter(c => !c.username)[0];
if (commentWithoutAnUsername(this.state.sortedComments) && !commentWithoutAnUsername(nextProps.comments)) {
sortedComments = updateSortedComments(
nextProps.comments,
currentProps.commentsSortOption,
nextProps.commentsvotes,
false
);
}
// commentsvotes changed
if(nextProps.commentsvotes && !isEqual(currentProps.commentsvotes, nextProps.commentsvotes)) {
const updatedComments = getUpdatedComments(nextProps.commentsvotes, nextProps.comments);
const newComments = mergeNewComments(this.state.sortedComments, updatedComments);
sortedComments = updateSortedComments(
newComments,
currentProps.commentsSortOption,
nextProps.commentsvotes,
false
);
}
// comment gets censored
if(nextProps.censoredComment && !isEqual(currentProps.censoredComment, nextProps.censoredComment)) {
sortedComments = updateSortedComments(
nextProps.comments,
currentProps.commentsSortOption,
nextProps.commentsvotes,
true
);
}
if(sortedComments) {
this.setState({ sortedComments });
console.log('setState', this.state.sortedComments);
}
}
render() {
const {
isLoading,
proposal,
token,
error,
markdownFile,
otherFiles,
onFetchData,
...props
} = this.props;
console.log(this.props);
const comments = this.state.sortedComments;
return (
<div className="content" role="main">
<div className="page proposal-page">
{error ? (
<Message
type="error"
header="Proposal not found"
body={error} />
) : (
<Content {...{
isLoading,
error,
bodyClassName: "single-page comments-page",
onFetchData: () => onFetchData(token),
listings: isLoading ? [] : [
{
allChildren: [{
kind: "t3",
data: {
...proposalToT3(proposal, 0).data,
otherFiles,
selftext: markdownFile ? getTextFromIndexMd(markdownFile) : null,
selftext_html: markdownFile ? getTextFromIndexMd(markdownFile) : null
}
}]
},
{ allChildren: commentsToT1(comments) }
],
...props
}} />
)}
</div>
</div>
);
}
}
export default ProposalDetail;
It’s not componentShouldUpdate but shouldComponentUpdate
shouldComponentUpdate basically decide whether the component requires re rendering or not. This method either returns true or false only. By default this method returns true which means the component needs re rendering always whenever setState happens or props received irrespective of the state and props comparison.
So in your case you are comparing comments incorrectly in shouldComponentUpdate. You need to return true only when current props and previous props are not equal otherwise false but you are checking vice versa.
The below code would work
shouldComponentUpdate(nextProps, nextState) {
console.log('thisProps', this.props.comments)
console.log('nextProps', nextProps.comments)
if (JSON.stringify(this.props.comments) !== JSON.stringify(nextProps.comments)) {
return true
}
else {
return false
}
}
Try this:
JSON.stringify(this.props.comments) === JSON.stringify(nextProps.comments)
This dirty hack could help.
The problem is that you cannot compare two arrays in such a way you do.
Related
im prety new to React and im trying to use an autocomplete input. Im having problems getting the value from it and clearing the input values after submitting. Any help would be greatly appretiated.
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import "../AutoComplete/styles.css"
class Autocomplete extends Component {
static propTypes = {
suggestions: PropTypes.instanceOf(Array)
};
static defaultProps = {
suggestions: [],
};
constructor(props) {
super(props);
this.state = {
// The active selection's index
activeSuggestion: 0,
// The suggestions that match the user's input
filteredSuggestions: [],
// Whether or not the suggestion list is shown
showSuggestions: false,
// What the user has entered
userInput: this.props.value ? this.props.value : "",
};
}
//Order by 'code'
generateSortFn(prop, reverse) {
return function (a, b) {
if (a[prop] < b[prop]) return reverse ? -1 : 1;
if (a[prop] > b[prop]) return reverse ? 1 : -1;
return 0;
};
}
onChange = e => {
const { suggestions } = this.props;
const userInput = e.currentTarget.value;
// Filter our suggestions that don't contain the user's input
const filteredSuggestions = suggestions.sort(this.generateSortFn('code', true)).filter(
(suggestion, i) => {
let aux = suggestion.descrp+"- "+suggestion.code
return aux.toLowerCase().indexOf(userInput.toLowerCase()) > -1
}
);
this.setState({
activeSuggestion: 0,
filteredSuggestions,
showSuggestions: true,
userInput: e.currentTarget.value
});
};
onClick = e => {
this.setState({
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText
});
};
onKeyDown = e => {
const { activeSuggestion, filteredSuggestions } = this.state;
// User pressed the enter key
if (e.keyCode === 13) {
this.setState({
activeSuggestion: 0,
showSuggestions: false,
userInput: filteredSuggestions[activeSuggestion].code+" - "+filteredSuggestions[activeSuggestion].descrp
});
}
// User pressed the up arrow
else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
this.setState({ activeSuggestion: activeSuggestion - 1 });
}
// User pressed the down arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
this.setState({ activeSuggestion: activeSuggestion + 1 });
}
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: {
activeSuggestion,
filteredSuggestions,
showSuggestions,
userInput
}
} = this;
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul className="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className="";
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion.code} onClick={onClick}>
{suggestion.code+" - "+suggestion.descrp}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div className="no-suggestions">
<p>Sin sugerencias</p>
</div>
);
}
}
and the return (this is where i think im wrong)
return (
<Fragment>
<label htmlFor="autocomplete-input" className="autocompleteLabel">{this.props.label}</label>
<div className="centerInput">
<input
className="autocomplete-input"
type="text"
onChange={onChange}
onKeyDown={onKeyDown}
defaultValue={this.props.initState}
value= {/* this.props.value ? this.props.value : */ userInput}
placeholder={this.props.placeholder}
selection={this.setState(this.props.selection)}
/>
{suggestionsListComponent}
</div>
</Fragment>
);
}
}
export default Autocomplete;
What I want is to use this component in different pages, so im passing the "selection" prop and setting the state there.
The input is working correctly (searches, gets the value and shows/hide the helper perfectly). The problem is i cant reset this inputs clearing them, and i suspect the error is in here.
I get the following warning (even with it somewhat functioning)
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
This is the Component usage with useState:
<Autocomplete label='Out cost Center:' placeholder='Set the out cost center' suggestions={dataCostCenterHelper} selection={(text) => setOutCostCenter(text.userInput)} value={outCostCenter} />
and last this is how im tryin to clear the state that is set in "selection":
const clearData = async () => {
setOutCostCenter('-');
// other inputs with the same component
setOutVendor('-');
setOutRefNumber('-');
}
This gets called inside the function that handles the button submitting the form.
Thanks in advance!
Looking at the code you posted this line might be the problem:
selection={this.setState(this.props.selection)}
You are updating state directly inside the render method, this is not recommended.
Try using a selection prop or state field and update the prop inside a componenteDidMount life cycle
selection={this.state.selection}
i want to render a div element only if condition1 or condition2 is true.
below is my code.
const Parent = () => {
const {notify} = useNotification();
return (
<SomeComponent
notify ({
actions: showInfo(item),
});
/>
);
}
const showInfo = (item) => {
return (
<>
{condition === 'value1' || condition === 'value2' ?
<div>header</div>
: undefined
}
</>
);
}
const useNotifications = () => {
const [activeNotifications, setActiveNotifications] = React.useContext(NotificationContext);
const notify = React.useCallback(
(notifications: Notification | Notification[]) => {
setActiveNotifications(activeNotifications => [
...activeNotifications,
]);
}
[setActiveNotifications]
);
return notify;
}
const Notification: React.FC<Props> = ({
description,actions}) => {
console.log('actions',actions) //here actions is printed false
return (
{(description || actions) && ( //why doesnt this condition work
<Body>//this is displayed even if actions is false
{actions && <div> {actions} </div>}
</Body>
}
);
}
})
in the above code the Body div is displayed even though actions is false as seen from console log statement.
if i change the condition to
{(description || actions === true) &&
<Body>//this is not displayed
{actions && <div>{actions}</div>}
</Body>
}
could someone help me understand why i have to explicitly check if {actions === true} and why {actions} doesnt work.
thanks.
EDIT: the type for the action is set like below
export type NotificationProps = {
actions?: React.ReactNode;
}
components/Settings/index.js
class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {
activateOption1: true,
activateOption2: true,
isPending: false,
dataPending: false,
activeData1: false
};
}
componentDidMount(){
const {pet, updatePetStore} = this.props;
this.setState({activeData1: updatePetStore.petstoreToggles.activeData1});
this.setState({activateOption1: updatePetStore.petstoreToggles.activateOption1});
}
componentUpdate(){
const {updatePetStores, dispatch, intid } = this.props;
if(updatePetStore.updatedPetStores == 'pending'){
if(updatePetStore.updatedNonPetCookie == false){
if(updatePetStore.selectedToggle == 'isActiveData1' && !this.state.dataPending){
this.setState({dataPending: true})
} else if(updatePetStore.selectedToggle == 'ActivateOption1' && !this.state.isPending) {
this.setState({isPending: true})
}
}
}
if((updatePetStore.updatedPetCookie && this.state.isPending == true) || (updatePetStore.updatedDataCookie && this.state.dataPending == true)){
if(this.state.isPending) {
dispatch(updatedPetCookie(false));
this.setState({isPending: !this.state.isPending});
this.setState({ActivateOption1: !this.state.ActivateOption1});
} else {
dispatch(updatedDataCookie(false));
this.setState({dataPending: !this.state.dataPending});
this.setState({activeData1: !this.state.activeData1});
}
}
}
render () {
const { petstoreName, pet, updatePetStore } = this.props;
const petstoreToggles = updatePetSTore.petstoreToggles;
var petstoreId = petstoreToggles.petstoreId;
var option2flag;
{petstoreToggles.ActivateOption1=='Yes' || petstoreToggles.ActivateOption1==true ? option2flag = true : option2flag = false};
var petChildrenOptions = null;
var dataChildrenOptions = null;
if(this.state.ActivateOption1) {
petChildrenOptions = (
<div id="options-children">
<Option
title="Main"
modalTitle="Main"
cookieName="ActivateMain"
petstore={petstoreName}
petstoreId={petstoreId}
active={petstoreToggles.ActivateMain}
/>
<Option
title="Option2"
modalTitle="Option2"
cookieName="ActivateOption2"
petstore={petstoreName}
petstoreId={petstoreId}
active={option2flag}
/>
<Option
title="Option3"
modalTitle="Option3"
cookieName="ActivateOption3"
petstore={petstoreName}
petstoreId={petstoreId}
active={true}
/>
<Option
title="Option4"
modalTitle="Option4"
cookieName="ActivateOption4"
petstore={petstoreName}
petstoreId={petstoreId}
active={petstoreToggles.ActivateOption4}
/>
</div>
);
components/Settings/Option/index.js
const cookie = new Cookies();
class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {
activate: false,
activateIsOpen: false,
update: false
};
}
componentWillMount(){
const { cookieName, active } = this.props;
const cookieDomain = getCookieDomain();
this.setState({activate: active});
}
componentDidUpdate() {
const { cookieName, updatePetStore, petstoreId, dispatch } = this.props;
const updatedpetstores = updatePetStore.updatedpetstores;
const cookieDomain = getCookieDomain();
var petstore = null;
if(updatePetStores.updatedCookieFlag && this.state.update){
if(cookieName == 'activeOption1' || cookieName == 'ActivateOption1'){
dispatch(updatedPetCookie(true));
}
for(var i = 0 ; i < updatedPetStores.length; i++) {
{updatedPetStores[i].uuid == petstoresId ? petstore = updatedPetStores[i] : null }
}
if (petstore != null) {
this.state.activate = petstore[cookieName];
if(petstore.accountId == cookie.get('accountId', {domain: cookieDomain})) {
cookie.set('ActivateOption1', petstore.ActivateOption1, {path: '/', domain: cookieDomain});
cookie.set('ActivateOption2', petstore.ActivateOption2, {path: '/', domain: cookieDomain});
cookie.set('ActivateMain', petstore.ActivateMain, {path: '/', domain: cookieDomain});
cookie.set('ActivateOption3', petstore.ActivateOption3, {path: '/', domain: cookieDomain});
}
}
dispatch(setCookieUpdateFlag(false));
this.setState({update: false});
}
}
toggleActivateIsOpen() {
this.setState({activateIsOpen: !this.state.activateIsOpen});
}
yesAction() {
const { dispatch, cookieName, petstoreId, router } = this.props;
if(cookieName != 'ActivateOption1' && cookieName != 'activeData1'){
dispatch(updatedNonPetCookie(true));
} else {
dispatch(selectedToggle(cookieName));
dispatch(updatedNonPetCookie(false));
}
dispatch(setActiveCookie(cookieName, petstoreId));
this.setState({activate: !this.state.activate});
this.setState({update: true});
this.toggleActivateIsOpen();
}
render () {
const { title, modalTitle, petstore, dispatch } = this.props;
const customStyles = {
content : {
marginRight : '-25%',
transform : 'translate(-25%, -25%)'
}
};
return (
<div>
<div className={this.state.activate == true ? "option-active" : "option"} onClick={() => this.toggleActivateIsOpen()}>
{title}
</div>
<Modal
isOpen={this.state.activateIsOpen}
shouldCloseOnOverlayClick={true}
style={customStyles}
contentLabel="Modal"
>
<div className="options-content">
<div>Are you sure you want to {this.state.isActive == true ? 'deactivate' : 'activate'} {modalTitle} for {petstore}?</div>
<div className="options-buttons">
<div className="yes" onClick={() => this.yesAction()}>Yes</div>
<div className="no" onClick={() => this.toggleActivateIsOpen()}>No</div>
</div>
</div>
</Modal>
</div>
)
}
}
function select(state) {
return {
updatePetStore: state.updatePetStore,
router: state.router
}
}
export default connect(select)(Settings);
The code rendered will look like this:
Option 1 [ ] // checkbox
Main [ ]
Option 2 [ ]
Option 3 [ ]
...
I can check the ticks on the checkbox, but whenever I refresh the checks do not save. How do I solve this issue?
I am guessing this is somehow related with cookies, but how?
Also, whenever I look into the developer console I am seeing an error on this page that is saying "Uncaught Invariant Violation". I am not sure if this is related, but it might be worth a mention. If I can provide more info, let me know.
You are not setting the state properly, please set the state like this,
this.setState({...this.state, activeData1: updatePetStore.petstoreToggles.activeData1});
this.setState({...this.state, activateOption1: updatePetStore.petstoreToggles.activateOption1});
you are not mutation the state but overriding it with activeData1: updatePetStore.petstoreToggles.activeData1, we need to send new reference to setState everytime need to change the state, contain initial and mutated data, so that it correctly tell React to render.
Example:
suppose we have initial state as
this.state = {
activateOption1: true,
activateOption2: true,
isPending: false,
dataPending: false,
activeData1: false
};
and now we are setting the state using setState api,
this.setState({activeData1: updatePetStore.petstoreToggles.activeData1});
this.setState({activateOption1: updatePetStore.petstoreToggles.activateOption1});
Now, our state structure becomes like this,
this.state = {
activateOptions1: value //mutated value
}
you loose all rest other properties it initially has.
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.
I am building a React app that - among other things - generates a random number when a button is clicked and then filters an array of JSON objects to only the one at the index of that random number (i.e. JSON[random]). Normally the app is supposed to re-render after the array of JSON objects is filtered, but for some reason, on the first time the button is clicked and a random is picked, it requires two clicks to update. From then on it updates as expected, with a new random rendering each time the button is clicked.
I'm not sure if the problem is coming from App.js or somewhere lower down. On the first click, it generates a new random and supposedly saves this to state, but fails to re-render right away. On subsequent clicks, things seem to update based on the previously-generated random, while a new random is put in the queue. I would prefer the this all happens in one go: click, generate random, save to state, update to reflect the new random à la JSON[random].
This might have something to do with the way I have implemented lifecycle methods, as I'm admittedly not sure of all the nuances of each and have just tried to use whichever ones seemed to do what I wanted. If you have any suggestions there, please let me know...
Thanks!
Here are the relevant files:
App.js - where the random is generated and stored when a new click is registered in Header.state.randomClicks
class App extends Component {
constructor(props){
super(props)
this.state = {headerLink: "", searchValue: "", random: 0, randomClicks: 0}
this.generateRandom = this.generateRandom.bind(this);
}
getLinkFromHeader = (link) => {
if (this.state.headerLink !== link) {
this.setState({
headerLink: link,
})
}
}
getSearchValueFromHeader = (string) => {
this.setState({
searchValue: string,
});
}
getRandomMax = (max) => {
this.setState({
randomMax: max,
})
}
getRandomClicks = (value) => {
this.setState({
randomClicks: value,
})
}
generateRandom(number) {
let random = Math.floor(Math.random() * number) + 1;
console.log("generateRandom = ", random)
return random
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.randomClicks !== nextState.randomClicks;
}
componentWillUpdate() {}
componentDidUpdate(prevState) {
let randomClicks = this.state.randomClicks;
console.log("this.state.randomClicks: ", this.state.randomClicks)
// console.log("prevState: ", prevState)
// console.log("prevState.randomClicks = ", prevState.randomClicks)
// ^^ is this a bug ? ^^
let random = this.generateRandom(this.state.randomMax);
if (this.state.random !== random) {
this.setState({random: random})
}
}
render() {
return (
<div className="App background">
<div className="content">
<Header getLinkFromHeader={this.getLinkFromHeader} getSearchValueFromHeader={this.getSearchValueFromHeader} randomClick={this.randomClick} getRandomClicks={this.getRandomClicks}/>
<TilesContainer link={this.state.headerLink} searchValue={this.state.searchValue} getRandomMax={this.getRandomMax} random={this.state.random} randomClicks={this.state.randomClicks}/>
</div>
</div>
);
}
}
export default App
Header.js* - where the randomClick count is incremented each time RandomButton is clicked
class Header extends Component {
constructor(props){
super(props);
this.state = { selectorLink: "", searchValue: "", randomClicks: 0 }
this.randomClick = this.randomClick.bind(this);
}
getLinkFromSelector = (link) => {
this.setState({
selectorLink: link,
})
}
getSearchValue = (string) => {
this.setState({
searchValue: string,
})
}
shouldComponentUpdate(nextProps, nextState) {
console.log("this.state !== nextState: ", this.state !== nextState)
return this.state !== nextState;
}
componentDidUpdate(previousState){
if(this.state.selectorLink !== previousState.selectorLink) {
this.props.getLinkFromHeader(this.state.selectorLink);
}
this.props.getSearchValueFromHeader(this.state.searchValue);
this.props.getRandomClicks(this.state.randomClicks);
console.log("Header Did Update")
}
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
render(){
return(
<div id="header" className="header">
<div className="title-div">
<div className="h1-wrapper title-wrapper">
<h1>Pokédex Viewer App</h1>
</div>
</div>
<PokedexSelector getLinkFromSelector={this.getLinkFromSelector}/>
<SearchBar getSearchValue={this.getSearchValue}/>
<button type="button" id="random-button" onClick={this.randomClick}>Random Pokémon</button>
<button type="button" id="show-all-button" onClick={this.showAllClick}>Show All</button>
</div>
)
}
}
export default Header
TilesContainer.js - where the random number from App is sent and the tiles list is filtered/re-rendered
class TilesContainer extends Component {
constructor(props){
super(props);
this.state = {
pokemon: [],
filteredPokemon: [],
randomMax: 0,
showDetails: false,
};
this.getPokemon = this.getPokemon.bind(this);
this.tiles = this.tiles.bind(this);
this.getPokemon(this.props.link);
}
getPokemon(pokedexLink) {
let link = "";
(pokedexLink === "")
? link = "https://pokeapi.co/api/v2/pokedex/national/"
: link = this.props.link;
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
this.setState({
pokemon: list,
randomMax: list.length,
})
this.props.getRandomMax; // send randomMax to App
})
}
filterPokemon(string) {
if (string !== "") {
console.log("string: ", string)
string = string.toString().toLowerCase()
let filteredPokemon = this.state.pokemon.filter(pokemon => {
const name = pokemon.pokemon_species.name;
const nameStr = name.slice(0,string.length);
const number = pokemon.entry_number;
const numberStr = number.toString().slice(0, string.length);
return (this.state.random !== 0) ? number.toString() === string : nameStr === string || numberStr === string;
})
if (this.props.randomClicks !== 0) { // i.e. using a random
this.setState({
filteredPokemon: filteredPokemon,
})
} else {
this.setState({
filteredPokemon: filteredPokemon,
randomMax: filteredPokemon.length,
})
}
} else {
this.setState({
filteredPokemon: [],
randomMax: this.state.pokemon.length,
})
}
}
componentDidUpdate(prevProps, prevState) {
if (this.props.link !== prevProps.link) {
this.getPokemon(this.props.link)
}
if (this.props.searchValue !== prevProps.searchValue) {
this.filterPokemon(this.props.searchValue)
}
if (this.state.randomMax !== prevState.randomMax){
this.props.getRandomMax(this.state.randomMax);
}
if (this.props.random !== prevProps.random) {
console.log("TilesContainer random: ", this.props.random)
this.filterPokemon(this.props.random)
}
}
tiles() {
console.log("tiles() filteredPokemon: ", this.state.filteredPokemon)
console.log("tiles() searchValue: ", this.props.searchValue)
console.log("tiles() random: ", this.props.random)
if (this.state.pokemon.length > 0) {
if (this.state.filteredPokemon.length == 0 && this.props.searchValue === ""){
return (
this.state.pokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
} else if (this.state.filteredPokemon.length > 0){
return (
this.state.filteredPokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
}
}
}
render(){
return (
<div id="tiles-container"
className="tiles-container">
{this.tiles()}
</div>
)
}
}
export default TilesContainer
You should not use current state in setState and should not modify state directly. And you do no actually call this.props.randomClick and it is undefined. Change
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
to
randomClick(){
if (typeof(this.props.randomClick) === 'function') this.props.randomClick();
this.setState(olState => ({
randomClicks: olState.randomClicks + 1,
}));
}
Also check your shouldComponentUpdate methods. They might be buggy or redundant. Looks like you prevent updating App when state.random changes. So every time you click the button you store the new random value but use the previous one. So for the initial render and for the first click you use random: 0.
And I guess that getRandomClicks should be setRandomClicks.