I have an app that has user profiles. On the user profile there are a list of friends, and when clicking on a friend it should take you to that other user profile.
Currently, when I click to navigate to the other profile (through redux-router Link) it updates the URL but does not update the profile or render the new route.
Here is a simplified code snippet, I've taken out a lot of code for simplicity sake. There are some more layers underneath but the problem happens at the top layer in my Profile Container. If I can get the userId prop to update for ProfileSections then everything will propagate through.
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) { this.props.getUser(userId) }
}
render() {
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
As you can see, what happens is that I am running the getUser action on componentWillMount, which will happen only once and is the reason the route changes but the profile data does not update.
When I change it to another lifecycle hook like componentWillUpdate to run the getUser action, I get in an endless loop of requests because it will keep updating the state and then update component.
I've also tried using the onEnter hook supplied by react-router on Route component but it doesn't fire when navigating from one profile to another since it's the same route, so that won't work.
I believe I'm thinking about this in the wrong way and am looking for some guidance on how I could handle this situation of navigating from one profile to another while the data is stored in the redux store.
So I would suggest you approach this in the following way:
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) {
// This is the initial fetch for your first user.
this.fetchUserData(userId)
}
}
componentWillReceiveProps(nextProps) {
const { userId } = this.props.params
const { userId: nextUserId } = nextProps.params
if (nextUserId && nextUserId !== userId) {
// This will refetch if the user ID changes.
this.fetchUserData(nextUserId)
}
}
fetchUserData(userId) {
this.props.getUser(userId)
}
render() {
const { user } = this.props
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
Note that I have it set up so in the componentWillMount lifecycle method, you make the request for the initial userId. The code in the componentWillReceiveProps method checks to see if a new user ID has been received (which will happen when you navigate to a different profile) and re-fetches the data if so.
You may consider using componentDidMount and componentDidUpdate instead of componentWillMount and componentWillReceiveProps respectively for the fetchUserData calls, but it could depend on your use case.
Related
I have a react component, let's call it Documents. In this component I need to load multiple pieces of dependent data using fetch. I am using redux thunk for these async redux actions that perform data fetching.
So the component looks like this:
interface Props {
comments: Entity[];
documents: Entity[];
users: Entity[];
getComments: Function;
getDocuments: Function;
getUsers: Function;
}
export class Documents<Props> {
public async componentDidMount(props: Props){
// is this the right lifecycle method for this since
// it does not really need to change state
const { documents, users, comments, getDocuments, getUsers, getComments} = props;
if(!documents || !documents.length){
await getDocuments();
} else {
await getUsers(documents.map(d => d.username));
}
getComments(documents, users); // dependent upon both users and documents
}
public render() {
// render documents, users, and comments
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Documents);
Here I need to look at the documents before I can load users. And Both users and documents before I can load comments. What's the best approach for handling this in react. My render function will load small sub-components responsible for rendering these data but I don't want them to be connected components.
I think I was able to accomplish it with a functional component/useEffect hooks:
https://codesandbox.io/s/festive-varahamihira-kpssd?file=/src/Documents.js
useEffect(() => {
if (!documents || !documents.length) {
getDocuments();
} else {
getUsers(documents);
}
}, [documents]);
useEffect(() => {
if (users) {
getComments(documents, users);
}
}, [users]);
I am relatively new to react, react native and redux and I have one question about fetching the data from the api and updating the state.
What I want to do is:
- each time a user opens the screen (i.e Posts) I want to fetch data from the api and update the state.
So that:
- I always have newly fetched data in the state and rendered on the screen.
If I fetch the data only in componentDidMount lifecycle method then I won't always have fresh data.
Example:
There are two screens Post and Posts. If a user opens posts screen for the first time then componentDidMount is triggered and posts are fetched and rendered. Then he goes to Post screen adds a new post and goes back to Posts screen then he will not see that new post because the posts will be displayed from the state fetched in componentDidMount method.
What is the best way to handle this and always have latest data?
class Posts extends Component {
componentDidMount() {
this.props.fetchPosts();
}
componentDidUpdate(prevProps) {
this.props.fetchPosts();
}
render () { ... }
}
const mapStateToProps = state => {
return {
posts: state.posts.posts,
loading: state.posts.loading,
}
}
const mapDispatchToProps = dispatch => {
return {
fetchPosts: () => dispatch(actions.fetchPosts()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Posts)
Action
export const fetchPosts = () => {
return dispatch => {
dispatch(fetchStarted());
apiFetchPosts()
.then(({data}) => {
const posts = data.data;
dispatch(fetched(posts))
})
.catch(error => {
dispatch(fetchFailed(error));
})
}
}
const fetchStarted = () => {
return {
type: actionTypes.POSTS_FETCH_START,
}
}
const fetched = (posts) => {
return {
type: actionTypes.POSTS_FETCH_SUCCESS,
posts,
}
}
const fetchFailed = (error) => {
return {
type: actionTypes.POSTS_FETCH_FAILED,
}
}
Update as you go
In your case, you should be sending a POST request to insert a new post. That request should have the newly created post as a response. You should use that response to update the list of posts you have in the redux state. Simply add the new post to the list.
The same goes for removing posts, once you send a DELETE request, you should also remove that post from the state.
Add timed cache
If you want to refetch the posts when you haven't fetched it for a while, you should implement some kind of caching solution:
When you fetch the posts for the first time you can also store the timestamp of the request. When the user visits the Posts page, you can check if the timestamp is too old e.g. 5 minutes in the past, and refetch it accordingly.
Refetch everytime
If you want to simply refetch the posts every time the user visits that page, you can use the useEffect hook or a combination of componentDidMount and componentDidUpdate.
Please note:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.
Source
You could use componentDidUpdate
I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()
In my react app I have component named profile, and I am fetching data from server and showing it inside that component. I am using redux and redux-thunk along with axios. With help of mapDispatchToProps function, i am calling redux action for fetching that data when component is mounted and saving it to redux state. After that, using mapStateToProps function i am showing that data on the screen via props. That works fine. Now I want to have possibility to edit, for example, first name of that user. To accomplish that i need to save that data to component state when data is fetched from server, and then when text field is changed, component state also needs to be changed. Don't know how to save data to component sate, immediately after it is fetched.
Simplified code:
const mapStateToProps = (state) => {
return {
user: state.user
}
}
const mapDispatchToProps = (dispatch) => {
return {
getUserData: () => dispatch(userActions.getUserData())
}
}
class Profile extends Component {
state:{
user: {}
}
componentDidMount (){
this.props.getUserData()
// when data is saved to redux state i need to save it to component state
}
editTextField = () => {
this.setState({
[e.target.id]: e.target.value
})
};
render(){
const { user } = this.props;
return(
<TextField id="firstName"
value={user.firstName}
onChange={this.editTextField}
/>
)
}
}
You can use componentDidUpdate for that or give a callback function to your action.
I will show both.
First lets see componentDidUpdate,
Here you can compare your previous data and your present data, and if there is some change, you can set your state, for example if you data is an array.
state = {
data: []
}
then inside your componentDidUpdate
componentDidUpdate(prevProps, prevState) {
if(prevProps.data.length !== this.props.data.length) {
// update your state, in your case you just need userData, so you
// can compare something like name or something else, but still
// for better equality check, you can use lodash, it will also check for objects,
this.setState({ data: this.props.data});
}
}
_.isEqual(a, b); // returns false if different
This was one solution, another solution is to pass a call back funtion to your action,
lets say you call this.props.getData()
you can do something like this
this.props.getData((data) => {
this.setState({ data });
})
here you pass your data from redux action to your state.
your redux action would be something like this.
export const getData = (done) => async dispatch => {
const data = await getSomeData(); // or api call
// when you dispatch your action, also call your done
done(data);
}
If you are using React 16.0+, you can use the static method getDerivedStateFromProps. You can read about it react docs.
Using your example:
class Profile extends Component {
// other methods here ...
static getDerivedStateFromProps(props) {
return {
user: props.user
}
}
// other methods here...
}
I may be missing something here, change in react-router params using Link will cause the props to change with the new param(s) and trigger an update cycle.
You can't dispatch an action in the update cycle but that's the only place where the parameter is available. I need to get new data when the day parameter changes in my stateless component.
The component has 2 links, previous and next day. The previous day looks like this:
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
[UPDATE]
Here is the solution so far:
The Overview component is just a function taking props and returning jsx, the following is the container that will check if date is set and if it's not it'll redirect. If date is set then it'll return Overview.
It also checks if the router day paramater changed, if it did then it'll set dispatchFetch to true.
This will then cause the render function to asynchronously dispatch the getData action.
Not sure if there would be another way to do this, I would prefer to listen to events from router and dispatch the events from there but there is no (working) way to listen to the router.
import { connect } from 'react-redux';
import React, { Component } from 'react';
import Overview from '../components/Overview';
import { getData } from '../actions';
import { selectOverview } from "../selectors";
import { Redirect } from "react-router-dom";
const defaultDate = "2018-01-02";
const mapStateToProps = (lastProps => (state, ownProps) => {
var dispatchFetch = false;
if (lastProps.match.params.day !== ownProps.match.params.day) {
lastProps.match.params.day = ownProps.match.params.day;
dispatchFetch = true;
}
return {
...selectOverview(state),
routeDay: ownProps.match.params.day,
dispatchFetch
}
})({ match: { params: {} } });
const mapDispatchToProps = {
getData
};
class RedirectWithDefault extends Component {
render() {
//I would try to listen to route change and then dispatch getData when
// routeDay changes but none of the methods worked
// https://github.com/ReactTraining/react-router/issues/3554
// onChange in FlexkidsApp.js never gets called and would probably
// result in the same problem of dispatching an event in update cycle
// browserHistory does not exist in react-router-dom
// this.props.history.listen didn't do anything when trying it in the constructor
//So how does one dispatch an event when props change in stateless components?
// can try to dispatch previous and next day instead of using Link component
// but that would not update the url in the location bar
if (this.props.dispatchFetch) {
Promise.resolve().then(() => this.props.getData(this.props.routeDay));
}
return (this.props.routeDay)//check if url has a date (mapStateToProps would set this)
? Overview(this.props)
: <Redirect//no date, redirect
to={{
pathname: "/list/" + defaultDate
}}
/>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RedirectWithDefault);
Intially I expect your Router is like,
<Route path="/sessions/:prefDay" component={MyComponent}/>
You will have to do this in getDerivedStateFromProps(),
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prefDay !== nextProps.match.params.prefDay) {
return {
prefDay: nextProps.match.params.prefDay,
};
}
// Return null to indicate no change to state.
return null;
}
Have you tried componentWillReceiveProps?
componentWillReceiveProps(props){
if(this.props.data != props.data && this.props.state.routeDay){
this.props.getData(props.state.routeDay);
}
}
To add parameter to route you need in router do this
<Route
path="/sessions/:prefDay"
....
/>
It means that now "/sessions/" rout have a parameter named "prefDay"
Example /sessions/:prefDay
and now in Link component need to add this parameter
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
You can get this value from url like this
this.props.match.params.prefDay
In RedirectWithDefaults which is a container of the stateless component I added componentDidMount (not an update cycle method):
const historyListener = (lastDay => (props,currentDay) => {
if(lastDay !== currentDay){
lastDay = currentDay;
if(!isNaN(new Date(currentDay))){
console.log("getting:",currentDay);
props.getData(currentDay);
}
}
})(undefined);
class RedirectWithDefault extends Component {
componentDidMount(history) {
this.props.getData(this.props.routeDay || defaultDate);
this.unListen = this.props.history.listen((location) => {
historyListener(
this.props,
location.pathname.split('/').slice(-1)[0]
);
});
}
componentWillUnmount(){
this.unListen();
}
Removed code from the render function. Now when it mounts it'll dispatch the action to load data and when the route changes it will dispatch it again but not in the update cycle.