I'm going to to click in div and show a text in another div in multiple items.
I have got series of data that contains some objects in one array(json file) and it will be shown by react.The code will be done up tohandelrule = ((e,element,i) =>{....}) . There is an onClick function ({e => this.handelrule(e,element,i)}) for each item.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
OtherRooms: {},
divVisibles: {},
loadingVisible: {},
resultRule: {},
};
}
render() {
const { data } = this.state;
const renderHotel = data.map((item, i) => {
return <div class="item">
<div class="moreInfo" onClick={(e) => this.showDiv(e, item, i)}><span>show more data</span></div>
<div key={i} className={`${!this.state.loadingVisible[i] ? "unvisible" : "visible"}`}>
<div id="ballsWaveG">
</div>
</div>
<div id="box-info" key={i} className={` ${!this.state.divVisibles[i] ? "unvisible" : "visible"}`}>
<div class="table">
{this.state.OtherRooms[i]}
</div>
</div>
</div>
});
return (
<div>
{renderHotel}
</div>
);
}
showDiv = (e, element, i) => {
this.showLoading(e, element, i);
setTimeout(() => {
fetch('/json.bc', {
method: 'POST'
})
.then(response => response.text())
.then(text => {
var Maindata = JSON.parse(text.replace(/\'/g, '"'))
this.setState(prevState => ({
Details: {
...prevState.Details,
[i]: this.renderDetails(Maindata, i),
},
divVisibles: { ...prevState.divVisibles, [i]: !prevState.divVisibles[i] },
loadingVisible: { ...prevState.loadingVisible, [i]: "" }
}))
}).catch(error => console.error(error))
}, 1000);
}
renderDetails(element, i) {
var indents = [];
indents.push(<div>
<span>{this.renderRule(element, i)}</span>
<div key={i} className={`${!this.state.loadingVisible[i] ? "unvisible" : "visible"}`}>
<div id="ballsWaveG">
</div>
</div>
<div key={i}>{this.state.resultRule[i]}</div>
</div>
)
return (
indents
)
}
showLoading = (e, elem, val) => {
this.setState(prevState => ({
loadingVisible: { ...prevState.loadingVisible, [val]: !prevState.loadingVisible[val] }
}))
};
renderRule(element, i) {
return <span class="txtRul" onClick={e => this.handelruleRoom(e, element, i)}>Show Rule</span>
}
handelruleRoom = (e, element, i) => {
var mainprovider = element.id.provider
if (mainprovider == undefined) {
return ''
} else {
this.showLoading(e, element, i);
/////the loading whould not be shown //////
setTimeout(() => {
var return_rule = function () { ////This part will be done but the result will not be shown in class="resultRule" ///////
var tmp = null;
$.ajax({
'async': false,
'type': "POST",
'global': false,
'dataType': 'html',
'url': "rule.bc",
'data': { 'mainprovider': JSON.stringify(mainprovider), },
'success': (response) => {
tmp = response;
}
});
return tmp;
}();
return this.setState(prevState => ({
resultRule: { ...prevState.resultRule, [i]: return_rule }, ///In this part return_rule does not set in resultRule ////
loadingVisible: { ...prevState.loadingVisible, [i]: "" }
}))
}, 1000);
}
}
}
ReactDOM.render(<App />, document.getElementById('Result'));
Actually there is a problem with this part this.setState( prevState => ({....})
There are quite a few issues with your code. I'm going to do my best to point them out and show alternatives, not merely to be critical of you, but to help you learn!
You're using quite a few state values. Think about the problem you're trying to solve, and determine the minimum possible states you need to accomplish that. For example, some of the conditional rendering your currently handling in state, can be offloaded to the render method itself using a ternary operator to render a loading component if a state value is undefined
render() {
<div>
{
!this.state.value
? <Loading />
: this.state.value
}
</div>
}
You're using class="name" in some elements. Because in Javascript, class is a keyword, when setting class names on JSX elements, use the className="name" property instead.
You're nesting your fetch calls inside of setTimeouts. This is not a very reliable method of ensuring the fetch returns a value, as network calls should be treated as taking an arbitrary amount of time. If it takes more than 1 second to fetch those files (for whatever reason) then your whole app will break. You have a few alternatives: You can add a series of callbacks, chain together .then()s, or use the new async/await ES6 feature. I'm going to give you an example of how you could utilize async/await for much cleaner and more reliable code:
showDiv = async (event, element, index) => {
this.showLoading(event, element, index);
let response = await fetch("/json.bc", { method: "POST" });
await response.text();
let data = JSON.parse(text.replace(/\'/g, '\"'));
this.setState(prevState => ({
Details: {
...prevState.Details,
[index]: this.renderDetails(data, index)
},
divVisible: {
...prevState.divVisibles,
[index]: !prevState.divVisibles[index]
},
loadingVisible: {
...prevState.loadingVisible,
[index]: ""
}
}));
};
Since this showDiv method itself is asynchronous, when you call it in your top-level code, you will need to chain a .then() and add additional code after it to ensure the new state has taken effect going forward
render() {
// Note that you cannot use await in this top-level code
showDiv.then(() => {
// The rest of your code that relies on the state set in showDiv
}).catch(err => console.error(err));
}
You may save yourself headache of dealing with asyncronous operations by simply importing the files containing your data directly into your app. The only reason you would need to use fetch() as you are, is if you were grabbing your data from a separate backend (like if you had a separate server for your database and REST API). Otherwise, you can pull those files into the client-side bundle with everything else:
// Use whatever the relative path is to these files
import data from "./json.bc";
import rules from "./rules.bc";
The power of React lies in its component based model. I see that your using a lot of helper functions within the same class to render different aspects of your interface. It may work for now, but it looks messy, and is confusing to debug and maintain. Instead, try extracting some of that functionality into new component classes, and then importing them into your App component to render. There will be a learning curve to figure out passing props, and changing state of a parent from a child, but you will gain the benefits of using React as it was intended. Most notably, you can avoid having to store arrays of data for every single div you're rendering. By encapsulating the functionality into a component, each component can manage it's own state and properties.
In your render() method, you placed a semicolon after a div near the end of your renderHotel function. Any JavaScript you want to place within a JSX block, must be put inside curly braces { }, and semicolons are not required to terminate JSX elements. It may have been a typo, but just in case wanted to add this in.
Using var may be more familiar to you, but it's spilling the variable scope all over the place, keeping variables that are no longer needed in memory because you're allowing them a class-wide scope. Instead, use const or let, the keep the variable context contained within their lexical scope. If you need access to one of those locally scoped variables, it can be solved through composition (the way you've organized your functions).
As to the problem you originally posted about, all of the problems I listed may be contributing; typos in JSX, class instead of className, async timing problems. Although perhaps the primary issue is regarding number 3. You are returning the value of tmp and binding it to return_rule possibly before the AJAX call has time to resolve. I would recommend refactoring your handleRuleRoom function using the async/await example I provided as a guide.
Your handelruleRoom function has no index declared inside its scope
So you just need to figure out what is index inside your handelruleRoom function which is probably the last argument of it you called it - i
Try changing index to i
like this
this.showLoading(e, DetailsRoomJ, i);
// and also here
return this.setState(prevState => ({
resultRule: { ...prevState.resultRule, [i]: return_rule },
loadingVisible: { ...prevState.loadingVisible, [i]: "" }
}))
Also if you care there are many things wrong with this snippet
class should be className
are you sure you need a sync function (because you are blocking you browser until you get your response)
Related
So I have a component where I have to make an API call to get some data that has IDs that I use for another async API call. My issue is I can't get the async API call to work correctly with updating the state via spread (...) so that the checks in the render can be made for displaying specific stages related to specific content.
FYI: Project is a Headless Drupal/React.
import WidgetButtonMenu from '../WidgetButtonMenu.jsx';
import { WidgetButtonType } from '../../Types/WidgetButtons.tsx';
import { getAllInitaitives, getInitiativeTaxonomyTerm } from '../../API/Initiatives.jsx';
import { useEffect } from 'react';
import { useState } from 'react';
import { stripHTML } from '../../Utilities/CommonCalls.jsx';
import '../../../CSS/Widgets/WidgetInitiativeOverview.css';
import iconAdd from '../../../Icons/Interaction/icon-add.svg';
function WidgetInitiativeOverview(props) {
const [initiatives, setInitiatives] = useState([]);
const [initiativesStages, setInitiativesStage] = useState([]);
// Get all initiatives and data
useEffect(() => {
const stages = [];
const asyncFn = async (initData) => {
await Promise.all(initData.map((initiative, index) => {
getInitiativeTaxonomyTerm(initiative.field_initiative_stage[0].target_id).then((data) => {
stages.push({
initiativeID: initiative.nid[0].value,
stageName: data.name[0].value
});
});
}));
return stages;
}
// Call data
getAllInitaitives().then((data) => {
setInitiatives(data);
asyncFn(data).then((returnStages) => {
setInitiativesStage(returnStages);
})
});
}, []);
useEffect(() => {
console.log('State of stages: ', initiativesStages);
}, [initiativesStages]);
return (
<>
<div className='widget-initiative-overview-container'>
<WidgetButtonMenu type={ WidgetButtonType.DotsMenu } />
{ initiatives.map((initiative, index) => {
return (
<div className='initiative-container' key={ index }>
<div className='top-bar'>
<div className='initiative-stage'>
{ initiativesStages.map((stage, stageIndex) => {
if (stage.initiativeID === initiative.nid[0].value) {
return stage.stageName;
}
}) }
</div>
<button className='btn-add-contributors'><img src={ iconAdd } alt='Add icon.' /></button>
</div>
<div className='initiative-title'>{ initiative.title[0].value } - NID ({ initiative.nid[0].value })</div>
<div className='initiative-description'>{ stripHTML(initiative.field_initiative_description[0].processed) }</div>
</div>
);
}) }
</div>
</>
);
}
export default WidgetInitiativeOverview;
Here's a link for video visualization: https://vimeo.com/743753924. In the video you can see that on page refresh, there is not data within the state but if I modify the code (like putting in a space) and saving it, data populates for half a second and updates correctly within the component.
I've tried using spread to make sure that the state isn't mutated but I'm still learning the ins and outs of React.
The initiatives state works fine but then again that's just 1 API call and then setting the data. The initiativeStages state can use X amount of API calls depending on the amount of initiatives are returned during the first API call.
I don't think the API calls are necessary for this question but I can give reference to them if needed. Again, I think it's just the issue with updating the state.
the function you pass to initData.map() does not return anything, so your await Promise.all() is waiting for an array of Promise.resolve(undefined) to resolve, which happens basically instantly, certainly long before your requests have finished and you had a chance to call stages.push({ ... });
That's why you setInitiativesStage([]) an empty array.
And what you do with const stages = []; and the stages.push() inside of the .then() is an antipattern, because it produces broken code like yours.
that's how I'd write that effect:
useEffect(() => {
// makes the request for a single initiative and transforms the result.
const getInitiative = initiative => getInitiativeTaxonomyTerm(
initiative.field_initiative_stage[0].target_id
).then(data => ({
initiativeID: initiative.nid[0].value,
stageName: data.name[0].value
}))
// Call data
getAllInitaitives()
.then((initiatives) => {
setInitiatives(initiatives);
Promise.all(initiatives.map(getInitiative))
.then(setInitiativesStage);
});
}, []);
this code still has a flaw (imo.) it first updates setInitiatives, then starts to make the API calls for the initiaives themselves, before also updating setInitiativesStage. So there is a (short) period of time when these two states are out of sync. You might want to delay setInitiatives(initiatives); until the other API requests have finished.
getAllInitaitives()
.then(async (initiatives) => {
const initiativesStages = await Promise.all(initiatives.map(getInitiative));
setInitiatives(initiatives);
setInitiativesStage(initiativesStages)
});
I'm trying to setup a component that lists a bunch of titles. Each title has a '+' next to it and when you click on the '+', another component will show that has more information. I know how to do this so that only one component can show at a time, but I wanted to make it so that multiple components could be viewed at once.
This is my code so far
import React, { useState, useEffect } from "react";
import MattressUpgrade from "./CamperMods/MattressUpgrade";
const CamperModsContainer = () => {
const [iconType, setIconType] = useState({
mattressUpgrade: "",
});
const [show, setShow] = useState({
mattressUpgrade: false,
});
useEffect(() => {
setIconType({
mattressUpgrade: "fas fa-plus-square",
});
setShow({
mattressUpgrade: false,
});
}, []);
let onClickHandler = (event) => {
event.preventDefault();
let payload = event.currentTarget;
toggleShow(payload);
};
const toggleShow = (payload) => {
debugger;
if (payload.value === true) {
setIconType.payload.id("fas fa-minus-square");
setShow.payload.id(true);
} else if (payload.value === false) {
setIconType.payload.id("fas fa-plus-square");
setShow.payload.id(false);
}
};
return (
<div>
<h1>Camper Modifications</h1>
<p>
As much as we love the Wolf Pup (aka the 'Gray Ghost'), there
are some things that we tweaked...
</p>
<div className="flex-row">
<h3>Mattress Upgrade</h3>
<button
type="button"
id={show.mattressUpgrade.key}
value={show.mattressUpgrade}
className={iconType.mattressUpgrade}
onClick={onClickHandler}
></button>
</div>
{show === true && <MattressUpgrade />}
</div>
);
};
export default CamperModsContainer;
The problem is that setting the id to id={show.mattressUpgrade.key} doesn't work. When I hit the debugger in the toggleShow I can access payload.value and get a value of false. But if I try to do payload.id it gives back an empty string and I can't figure out why.
I believe you are looking for computed properties, but there are several other misunderstandings here that are worth highlighting.
A general suggestion is to try to keep datatypes consistent when possible. Converting between a string an boolean is hard to follow and debug. If you end up switching between more complex datatypes (objects, etc) it becomes very error prone.
The first problem is that show.mattressUpgrade.key does not work, and it will never work. key is accessed as an object property on show.mattressUpgrade, but show.mattressUpgrade is not an object, its false. You can expand and run the below snippet to see that accessing a property on false yields undefined, and assigning undefined as the input's id results in e.target.id of an empty string.
const Example = () => {
const test = false;
console.log('test.property', test.property);
const handleChange = (e) => {
console.log(e.target.id)
}
return <input id={test.property} onChange={handleChange} />
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I can't tell you how you should set the id for the button's in your code because there is only one in your example, but this is why id does not work.
As for the updating state dynamically, setShow.payload.id this is not the right way to go. setShow is a function, not an object. Even if it were, you would not be referencing it by the variable payload.id, you would be attempting to access a literal nested property called "payload" then "id". Even further, id is not expected to be a function, but you still call it as one.
To access object properties dynamically, you use array syntax (ojb['property']). Your function would then be something like this:
const toggleShow = (payload) => {
debugger;
if (payload.value === true) {
setIconType(prev => ({...prev, [payload.id]: "fas fa-minus-square"}));
setShow(prev => ({...prev, [payload.id]: true}));
} else if (payload.value === false) {
setIconType(prev => ({...prev, [payload.id]: "fas fa-plus-square"}));
setShow(prev => ({...prev, [payload.id]: false}));
}
};
Notice we also use a functional update and spread prev into the new object in order to keep any values that are not explicitly replaced.
List of items to render
Given a list of items (coming from the server):
const itemsFromServer = {
"1": {
id: "1",
value: "test"
},
"2": {
id: "2",
value: "another row"
}
};
Function component for each item
We want to render each item, but only when necessary and something changes:
const Item = React.memo(function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
});
ItemList function component with a handleSave function that needs to be memoized.
And there is a possibility to save each individual item:
function ItemList() {
const [items, setItems] = useState(itemsFromServer);
const handleChange = useCallback(
function handleChange(id, value) {
setItems(currentItems => {
return {
...currentItems,
[id]: {
...currentItems[id],
value
}
};
});
},
[setItems]
);
async function handleSave(id) {
const item = items[id];
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
await save(item);
alert("Save done :)");
}
return (
<ul>
{Object.values(items).map(item => (
<Item
key={item.id}
id={item.id}
value={item.value}
onChange={handleChange}
onSave={handleSave}
/>
))}
</ul>
);
}
How to prevent unnecessary re-renders of each Item when only one item changes?
Currently on each render a new handleSave function is created. When using useCallback the items object is included in the dependency list.
Possible solutions
Pass value as parameter to handleSave, thus removing the items object from the dependency list of handleSave. In this example that would be a decent solution, but for multiple reasons it's not preferred in the real life scenario (eg. lots more parameters etc.).
Use a separate component ItemWrapper where the handleSave function can be memoized.
function ItemWrapper({ item, onChange, onSave }) {
const memoizedOnSave = useCallback(onSave, [item]);
return (
<Item
id={item.id}
value={item.value}
onChange={onChange}
onSave={memoizedOnSave}
/>
);
}
With the useRef() hook, on each change to items write it to the ref and read items from the ref inside the handleSave function.
Keep a variable idToSave in the state. Set this on save. Then trigger the save function with useEffect(() => { /* save */ }, [idToSave]). "Reactively".
Question
All of the solutions above seem not ideal to me. Are there any other ways to prevent creating a new handleSave function on each render for each Item, thus preventing unnecessary re-renders? If not, is there a preferred way to do this?
CodeSandbox: https://codesandbox.io/s/wonderful-tesla-9wcph?file=/src/App.js
The first question I'd like to ask : is it really a problem to re-render ?
You are right that react will re-call every render for every function you have here, but your DOM should not change that much it might not be a big deal.
If you have heavy calculation while rendering Item, then you can memoize the heavy calculations.
If you really want to optimize this code, I see different solutions here:
Simplest solution : change the ItemList to a class component, this way handleSave will be an instance method.
Use an external form library that should work fine: you have powerfull form libraries in final-form, formik or react-hook-form
Another external library : you can try recoiljs that has been build for this specific use-case
Wow this was fun! Hooks are very different then classes. I got it to work by changing your Item component.
const Item = React.memo(
function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
},
(prevProps, nextProps) => {
// console.log("PrevProps", prevProps);
// console.log("NextProps", nextProps);
return prevProps.value === nextProps.value;
}
);
By adding the second parameter to React.memo it only updates when the value prop changes. The docs here explain that this is the equivalent of shouldComponentUpdate in classes.
I am not an expert at Hooks so anyone who can confirm or deny my logic, please chime in and let me know but I think that the reason this needs to be done is because the two functions declared in the body of the ItemList component (handleChange and handleSave) are in fact changing on each render. So when the map is happening, it passes in new instances each time for handleChange and handleSave. The Item component detects them as changes and causes a render. By passing the second parameter you can control what the Item component is testing and only check for the value prop being different and ignore the onChange and onSave.
There might be a better Hooks way to do this but I am not sure how. I updated the code sample so you can see it working.
https://codesandbox.io/s/keen-roentgen-5f25f?file=/src/App.js
I've gained some new insights (thanks Dan), and I think I prefer something like this below. Sure it might look a bit complicated for such a simple hello world example, but for real world examples it might be a good fit.
Main changes:
Use a reducer + dispatch for keeping state. Not required, but to make it complete. Then we don't need useCallback for the onChange handler.
Pass down dispatch via context. Not required, but to make it complete. Otherwise just pass down dispatch.
Use an ItemWrapper (or Container) component. Adds an additional component to the tree, but provides value as the structure grows. It also reflects the situation we have: each item has a save functionality that requires the entire item. But the Item component itself does not. ItemWrapper might be seen as something like a save() provider in this scenario ItemWithSave.
To reflect a more real world scenario there is now also a "item is saving" state and the other id that's only used in the save() function.
The final code (also see: https://codesandbox.io/s/autumn-shape-k66wy?file=/src/App.js).
Intial state, items from server
const itemsFromServer = {
"1": {
id: "1",
otherIdForSavingOnly: "1-1",
value: "test",
isSaving: false
},
"2": {
id: "2",
otherIdForSavingOnly: "2-2",
value: "another row",
isSaving: false
}
};
A reducer to manage state
function reducer(currentItems, action) {
switch (action.type) {
case "SET_VALUE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
value: action.value
}
};
case "START_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: true
}
};
case "STOP_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: false
}
};
default:
throw new Error();
}
}
Our ItemList to render all items from the server
export default function ItemList() {
const [items, dispatch] = useReducer(reducer, itemsFromServer);
return (
<ItemListDispatch.Provider value={dispatch}>
<ul>
{Object.values(items).map(item => (
<ItemWrapper key={item.id} item={item} />
))}
</ul>
</ItemListDispatch.Provider>
);
}
The main solution ItemWrapper or ItemWithSave
function ItemWrapper({ item }) {
const dispatch = useContext(ItemListDispatch);
const handleSave = useCallback(
// Could be extracted entirely
async function save() {
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
dispatch({ type: "START_SAVE", id: item.id });
// Save to API
// eg. this will use otherId that's not necessary for the Item component
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: "STOP_SAVE", id: item.id });
},
[item, dispatch]
);
return (
<Item
id={item.id}
value={item.value}
isSaving={item.isSaving}
onSave={handleSave}
/>
);
}
Our Item
const Item = React.memo(function Item({ id, value, isSaving, onSave }) {
const dispatch = useContext(ItemListDispatch);
console.log("render", id);
if (isSaving) {
return <li>Saving...</li>;
}
function onChange(event) {
dispatch({ type: "SET_VALUE", id, value: event.target.value });
}
return (
<li>
<input value={value} onChange={onChange} />
<button onClick={onSave}>Save</button>
</li>
);
});
I have no clue why the selected dropdown value is one step behind in the URL search params string. My url is like this: http://localhost/?dropdownsel=. Below is my code:
//App.js
//update params value
function setParams({ dropdownsel }) {
const searchParams = new URLSearchParams();
searchParams.set("dropdownsel", dropdownsel);
return searchParams.toString();
}
class App extends Component {
state = {
dropdownsel: ""
};
//update url params
updateURL = () => {
const url = setParams({
dropdownsel: this.state.dropdownsel
});
//do not forget the "?" !
this.props.history.push(`?${url}`);
};
onDropdownChange = dropdownsel => {
this.setState({ dropdwonsel: dropdownsel });
this.updateURL();
};
render() {
return (
<Dropdownsel
onChange={this.onDropdownselChange}
value={this.state.dropdownsel}
/>
);
}
}
Below is dropdownsel component code:
//Dropdownsel.js
const attrData = [{ id: 1, value: AA }, { id: 2, value: BB }];
class Dropdownsel extends Component {
onDropdownChange = event => {
this.props.onChange(event.target.value);
};
render() {
return (
<div>
<select value={this.props.value} onChange={this.onDropdownChange}>
<option value="">Select</option>
{attrData.map(item => (
<option key={item.id} value={item.value}>
{" "}
{item.name}
</option>
))}
</select>
</div>
);
}
}
export default Dropdownsel;
Thanks for formatting my code. I don't know how to do it every time when I post question. I figured it out myself. I need to make a call back function for updateURL() because the setState() is not executed immediately. so my code should be revised like below:
onDropdownChange = (dropdownsel) => {
this.setState({ dropdwonsel:dropdownsel }, ()=>{this.updateURL();
});
};
The problem occurs because this.setState is asynchronous (like a Promise or setTimeout are)
So there are two workarounds for your specific case.
Workaround #1 - using a callback
Use the callback option of this.setState.
When you take a look at the declaration of setState, it accepts an "optional" callback method, which is called when the state has been updated.
setState(updater[, callback])
What it means is that, within the callback, you have an access to the updated state, which was called asynchronously behind the scene by React.
So if you call this.updateURL() within the callback, this.state.dropdownsel value will be the one you are expecting.
Instead of,
this.setState({ dropdwonsel: dropdownsel });
this.updateURL();
Call this.updateURL in the callback.
// Note: '{ dropdwonsel }' is equal to '{ dropdwonsel: dropdwonsel }'
// If your value is same as the state, you can simplify this way
this.setState({ dropdwonsel }, () => {
this.updateURL()
});
Workaround #2 - passing the new value directly
You can also pass the new value directly as an argument of this.updateURL() (which might make testing easier as it makes you method depend on a value, which you can fake).
this.setState({ dropdwonsel });
this.updateURL(dropdownsel );
Now your this.updateURL doesn't depend on the this.state.dropdownsel, thus you can can use the arg to push the history.
//update url params
updateURL = dropdownsel => {
const url = setParams({
dropdownsel
});
//do not forget the "?" !
this.props.history.push(`?${url}`);
};
So the problem is as follows: I have a search function that get's an default value passed on from another place, the search works, but only when it gets a new input, hence if I'm passing "Dress" it wont call my api function before i change something in the input.
I've tried a bit of everything like setInitialState(), but without any noteworthy success.
As you can see I'm getting a onTermChange from my Searchbar that's passed to handleTermChange which then updates my products:[], but I need this.props.location.query to be the default search term, as this is the passed on variable.
handleTermChange = (term) => {
const url = `http://localhost:3001/products?title=${term.replace(/\s/g, '+')}`;
request.get(url, (err, res) => {
this.setState({ products: res.body })
});
};
render () {
return (
<div className='col-md-12' style={{ margin: '0 auto' }}>
<div className='row searchPageHeader' style={{ padding: '10px', backgroundColor: '#1ABC9C' }}/>
<SideMenu />
<SearchBar onTermChange={this.handleTermChange}
defaultValue={this.props.location.query}/>
<ProductList products={this.state.products}
onProductSelect={selectedProduct => this.openModal(selectedProduct)}/>
<ProductModal modalIsOpen={this.state.modalIsOpen}
selectedProduct={this.state.selectedProduct}
onRequestClose={ () => this.closeModal() }/>
<Footer />
</div>
);
}
I would personally just do the same logic in componentDidMount(), like this:
componentDidMount () {
const url = `http://localhost:3001/products?title=${this.props.location.query}`;
request.get(url, (err, res) => {
this.setState({ products: res.body })
});
}
Note that since you are doing an asynchronous call products won't be populated from the API result until a moment after the component is mounted. Make sure you initialize products in initialState (I assume this returns an array, so initialize it as an empty array).
Opinion: Since you are following the event handler naming conventions (i.e onX followed by handleX) I would avoid calling handleTermChange() inside componentDidMount() because the function name suggests it's bound to an event listener. So calling it directly is just bad practice in my opinion. So if you'd rather call a function in here, rather than writing out the logic like I did above, I would do the following:
componentDidMount() {
this.changeTerm(this.props.location.query);
}
changeTerm = (term) => {
const url = `http://localhost:3001/products?title=${term.replace(/\s/g, '+')}`;
request.get(url, (err, res) => {
this.setState({ products: res.body })
});
};
handleTermChange = (term) => {
this.changeTerm(term);
}
Your render() remains unchanged. Maybe a stretch, but I prefer it this way.