React.js - Is there a way to grab props.match.params value before the render() method? - reactjs

I have a Post class which will dynamically display content from each post from the database depending on the params. My issue is that I want to use state, but the only time the match.params id value is available is during the render method - at which point I can't use set state because of infinite loops. As a work around I'm using local variables and only setting them in render if match.params is not null. But I ideally want to use state.
Is there a way to grab that param before the render method so i can use set state?
UPDATED TO SHOW CODE:
This bit is here is where I check to see the param and match it with the post from db:
render() {
if(this.state.props.match) {
post = this.props.data.posts.find(post => post.id == this.props.match.params.id );
}
}
Because match.params is only available at this stage, I can't do it when the component first loads as it would be null.
In that post object I have a comments object, and again I would like to assign but can't. So what I'm having to do is assign it to state when I make a POST request (thus a method and not render):
postComment = (e, postId, postComments) => {
e.preventDefault();
let data = {
comment: this.state.comment,
post_id: postId
}
if(this.state.comment != "") {
axios.post('http://127.0.0.1:8000/api/post_comments', data)
.then(response => {
// handle success
console.log(response);
postComments.unshift(response.data);
this.setState({
comments: postComments
});
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
}
This means that to display the comments, I then have to compare state with the comments object in post. If state is bigger than the other, i.e. a new value has been entered, map that one, else map the other otherwise existing comments won't get displayed:
{
this.state.comments > post.comments ?
this.state.comments.map(comment =>
<p key={comment.id}>{comment.comment}</p>
)
:
post.comments.map(comment =>
<p key={comment.id}>{comment.comment}</p>
)
}
It just feels a bit messy and working with one array in state would be cleaner.
this.state.props.match:
{path: "/blog/:id(\d+)", url: "/blog/2", isExact: true, params: {…}}
isExact: true
params: {id: "2"}
path: "/blog/:id(\d+)"
url: "/blog/2"}
So when clicking on post 2 (blog/2), the above will print, when clicking on 3, id would be 3 and so on

ok, so i had something similar to your problem. In general if your state depends on props, you can try to use getDerivedStateFromProps.
static getDerivedStateFromProps(props, state) {
post = props.data.posts.find(post => post.id == props.match.params.id );
if (post !== state.post) {
//add your magic here
return {
//return a new state....this is like calling setState
};
}
return null; // do nothing
}

Related

React state contains correct data initially but is overwritten by previous value

i have a state variable which is initially set to null. After getting data from server i update its state. In doing so.. it loads correct data for items with different ids. however on opening the item that was previously opened... sets the state with correct value. however it then overwrites the state value with previous opened item.
I am not sure what going wrong.
Below is the code,
class Items extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
item_details: null,
};
componentDidMount() {
this.load_item_details();
}
componentDidUpdate(PrevProps) {
this.load_item_details();
}
load_item_details = () => {
const file_name = 'item_details.json';
client.get_item_details_file(this.props.item_id, file_name, 'json')
.then((request) => {
this.setState({item_details: request.response});
})};
}
Could someone help me solve this. thanks.
Edit: This is how the output looks the first time i run the application clicking some item for the first time.
Now when i click another item with id 8 it outputs correct data (from output state with array 25) however it is replaced with previous item details (from output state with array 806).
You have item_id inside method 1 and item_id inside method 8 in the second log. Looks like you send two simultaneous requests with different ids. Since data for item_id=1 is much larger than data for item_id=8 it arrives later and overwrites everything. Try to call load_item_details only when needed and use some flag to block simultaneous requests.
...
this.state = {
item_details: null,
isLoading: false,
};
...
componentDidUpdate(PrevProps) {
if (PrevProps.item_id !== this.props.item_id) this.load_item_details();
}
...
load_item_details = () => {
if (this.state.isLoading) return;
const file_name = 'item_details.json';
this.setState({isLoading: true});
client.get_item_details_file(this.props.item_id, file_name, 'json')
.then((request) => {
this.setState({item_details: request.response, isLoading: false});
})};
}
Note that this code will not react to any changes to item_id while load_item_details is fetching data. And it will break on error inside client.get_item_details_file.
You probably have a typo on your code:
load_item_details = () => {
const file_name = 'item_details.json';
client.get_item_details_file(this.props.item_id, file_name, 'json')
.then((request) => {
this.setState({item_detail: request.response});
})};
}
It should be this.setState({item_details: request.response})

State Updates After Second Load of componentWillReceiveProps and will not map

I am passing properties from a parent to the child and want to map the resulting array.
When I try to console log the result, the properties only reach after the second time the function is called. The first time it returns "[]". Even when the state "users" array is recieved, mapping it does nothing.
state={
users: null
}
componentWillReceiveProps(){
var userInLobby = this.props.users && this.props.users.map(userdata=>{
if(userdata.status === "online"){
var user =
firebase.firestore().collection("users").doc(userdata.id).get().then(user=>{
return {
username: user.data().username,
};
})
return user
}else{
return null;
}
})
var userInLobbyDisplay = []
userInLobby && Promise.all(userInLobby).then(values =>
values.map(user=>{
return userInLobbyDisplay.push(user)
}))
this.setState({
users: userInLobbyDisplay
})
}
render(){
console.log(this.state.users)
return(
<div className="lobby">
{
this.state.users && this.state.users.map(user=>{
return user.username
})
}
</div>
)
How can I map the users array from the state to the DOM when receiving props for the first time?
You are using a soon to be deprecated API, you should follow the documentation guidelines and switch to the newer APIs, specifically the new getDerivedStateFromProps You can read more here.
static getDerivedStateFromProps(props, state) {
// Your code here
}

Cannot access state from ComponentDidMount() [duplicate]

This question already has an answer here:
setState doesn't update the state immediately
8 answers
In componentDidMount() I'm calling setQueryParams to set the query parameters into state which works fine.
componentDidMount() {
this.setQueryParams();
console.log(this.state.query);
$.getJSON(`/search?${this.state.query}`, (response) => {
this.addDiveCenters(response.centers)
this.buildFilters(response.filters, false)
});
console.log(this.state.query);
}
However, after the setQueryParams() call, console.log(this.state.query) is returning '' (the default state). After the page loads I can run $r.state.query and everything is perfect,but for some reason I cannot access the state! I need the initial state to make an AJAX call. Right now, every single page loads to /search because this.state.query is blank.
setQueryParams = (param = null, value = null) => {
const query = this.state.query;
// get current location
const location = this.props.location.search;
// Set params based on whether component mount or filter change
const params = param ? new URLSearchParams(query) : new URLSearchParams(location);
if (param) {
params.set(param, value);
}
this.setState({
query: params.toString()
});
console.log(this.state.query); // doesn't display anything!
}
It will take some time because this.setState behaves asynchronous
this.setState has callback method which notify that state has been updated
this.setState({
query: params.toString()
},() => {
// Do something here.
});
Read this
https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
Solution :-
componentDidMount() {
this.setQueryParams(null, null, () => {
console.log(this.state.query);
$.getJSON(`/search?${this.state.query}`, (response) => {
this.addDiveCenters(response.centers)
this.buildFilters(response.filters, false)
});
console.log(this.state.query);
});
}
Pass callback method in setQueryParams
setQueryParams = (param = null, value = null,callback) => {
const query = this.state.query;
// get current location
const location = this.props.location.search;
// Set params based on whether component mount or filter change
const params = param ? new URLSearchParams(query) : new URLSearchParams(location);
if (param) {
params.set(param, value);
}
this.setState({
query: params.toString()
},callback);
console.log(this.state.query); // doesn't display anything!
}
State Updates May Be Asynchronous
The setState is an Async function, so it gets time to set your value as state and when you log the state value, the new value hasn't been set yet.
From the react docs:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
This is why you are not getting the response you expect immediately. However, somewhat confusingly, there are times when set state is immediate. However, you should never expect it to be.
Since the state change is asynchronous, react provides a way to, ahem, react to the state after it has been changed, by means of a callback. So you can write code like this:
this.setState({
query: params
},(updatedState) => {
console.log("updated state:", updatedState)
});

React-redux project - chained dependent async calls not working with redux-promise middleware?

I'm new to using redux, and I'm trying to set up redux-promise as middleware. I have this case I can't seem to get to work (things work for me when I'm just trying to do one async call without chaining)
Say I have two API calls:
1) getItem(someId) -> {attr1: something, attr2: something, tagIds: [...]}
2) getTags() -> [{someTagObject1}, {someTagObject2}]
I need to call the first one, and get an item, then get all the tags, and then return an object that contains both the item and the tags relating to that item.
Right now, my action creator is like this:
export function fetchTagsForItem(id = null, params = new Map()) {
return {
type: FETCH_ITEM_INFO,
payload: getItem(...) // some axios call
.then(item => getTags() // gets all tags
.then(tags => toItemDetails(tags.data, item.data)))
}
}
I have a console.log in toItemDetails, and I can see that when the calls are completed, we eventually get into toItemDetails and result in the right information. However, it looks like we're getting to the reducer before the calls are completed, and I'm just getting an undefined payload from the reducer (and it doesn't try again). The reducer is just trying to return action.payload for this case.
I know the chained calls aren't great, but I'd at least like to see it working. Is this something that can be done with just redux-promise? If not, any examples of how to get this functioning would be greatly appreciated!
I filled in your missing code with placeholder functions and it worked for me - my payload ended up containing a promise which resolved to the return value of toItemDetails. So maybe it's something in the code you haven't included here.
function getItem(id) {
return Promise.resolve({
attr1: 'hello',
data: 'data inside item',
tagIds: [1, 3, 5]
});
}
function getTags(tagIds) {
return Promise.resolve({ data: 'abc' });
}
function toItemDetails(tagData, itemData) {
return { itemDetails: { tagData, itemData } };
}
function fetchTagsForItem(id = null) {
let itemFromAxios;
return {
type: 'FETCH_ITEM_INFO',
payload: getItem(id)
.then(item => {
itemFromAxios = item;
return getTags(item.tagIds);
})
.then(tags => toItemDetails(tags.data, itemFromAxios.data))
};
}
const action = fetchTagsForItem(1);
action.payload.then(result => {
console.log(`result: ${JSON.stringify(result)}`);
});
Output:
result: {"itemDetails":{"tagData":"abc","itemData":"data inside item"}}
In order to access item in the second step, you'll need to store it in a variable that is declared in the function scope of fetchTagsForItem, because the two .thens are essentially siblings: both can access the enclosing scope, but the second call to .then won't have access to vars declared in the first one.
Separation of concerns
The code that creates the action you send to Redux is also making multiple Axios calls and massaging the returned data. This makes it more complicated to read and understand, and will make it harder to do things like handle errors in your Axios calls. I suggest splitting things up. One option:
Put any code that calls Axios in its own function
Set payload to the return value of that function.
Move that function, and all other funcs that call Axios, into a separate file (or set of files). That file becomes your API client.
This would look something like:
// apiclient.js
const BASE_URL = 'https://yourapiserver.com/';
const makeUrl = (relativeUrl) => BASE_URL + relativeUrl;
function getItemById(id) {
return axios.get(makeUrl(GET_ITEM_URL) + id);
}
function fetchTagsForItemWithId(id) {
...
}
// Other client calls and helper funcs here
export default {
fetchTagsForItemWithId
};
Your actions file:
// items-actions.js
import ApiClient from './api-client';
function fetchItemTags(id) {
const itemInfoPromise = ApiClient.fetchTagsForItemWithId(id);
return {
type: 'FETCH_ITEM_INFO',
payload: itemInfoPromise
};
}

ReactJS - mapped array not updating after ajax

I've got a simple component that renders a table. The rows are mapped like this:
render () {
return (
{this.state.data.map(function(row, i){
return <Row row={row} key={i}/>
}.bind(this))}
)
}
The state is initialized in the constructor:
this.state = {
data: props.hasOwnProperty('data') ? props.data : [],
route: props.hasOwnProperty('route') ? props.route : null
}
The data can be initialized in the DOM, or after, the route is passed to the container and bound correctly. In this case, I 'm doing it after in the componentDidMount method:
componentDidMount () {
axios.get(this.state.route)
.then(function(resp){
this.setStateParametersFromAjax(resp);
}.bind(this))
.catch(function(err){
console.log(err);
})
}
The setStateParametersFromAjax(resp) method is defined here:
this.setState({
data: resp.data.data,
});
This all works flawlessly on DOM load. However, there are buttons that will perform subsequent requests later on. These requests perform the same axios call.
The problem is, that even though the state is updated (verified by adding a callback as the 2nd argument to the setState method and logging this.state), the DOM does not update.
What do I need to do to make it so that the DOM updates with this new data as well?
Edit
I had simplified the code a bit, there is a method called fetch() that accepts an argument of params that defines the ajax call:
fetch (params) {
if(typeof params != "object") {
params = {};
}
axios.get(this.state.route, {
params
}).then(function(resp) {
this.setStateParametersFromAjax(resp);
}.bind(this))
.catch(function(err){
console.log(err);
})
}
The componentDidMount() method calls this on load:
componentDidmMount () {
this.fetch();
}
When a button is clicked later, this calls a function that calls the fetch method with parameters:
<li className="page-item" onClick={this.getNextPage.bind(this)}>
Where the function is defined:
getNextPage (event) {
event.preventDefault();
this.fetch({
arg: val
});
}
You should use a unique key value other than index. Even the state is updated, react may not update DOM if the key not changed.
https://facebook.github.io/react/docs/reconciliation.html

Resources