I have a React application which uses Redux as state container.
I have a REST API which I use to load data into the app.
Some of the data only contains IDs and to show the real content behind the IDs I need to fetch more data from the REST API.
To make an example:
I fetch a list of events. These events have speakers and participants.
When I click on an event in the app it shows the speaker and also some of the participants, but it's only a preview of them. So not all of them are shown. To show a person, with its image and a name, I need to make another API call.
My question is what is the best way to structure my code so I just need to call the API for the persons that are shown. So if I click on one event I only need to load the persons involved in this event, and also only the ones that are previewed.
I also try to have my components which render the persons not depend on the Redux store but be just plain React components which get their state from their props.
I can provide more details if needed.
You can simply load your extra data using componentDidMount make the API call and set the state locally or dispatch an async Action Creator in the componentDidMount, store the event in your redux store and retrieve it. Depends if you want to use the event detail data in other parts of your application or it is only used in that particular view, and what your personal preference is of course.
Using local state ->
class EventDetail extends React.Component {
constructor() {
super(props);
this.state = {
event: null
};
}
componentDidMount() {
// Get event id from passed prop
const id = this.props.eventId;
// Or using react-router, so you can directly link to the URL
// https://reacttraining.com/react-router/web/example/url-params
const id = this.props.match.params.eventId;
// Make API call using whatever you are using
// HTTP GET 'events/${id}' -> Details including persons
apiCallToGetEventDetailsIncludingPersons(id)
.then(event => { this.setState({ event: event }); })
.catch(err => { /* handle error */ } );
}
render() {
// Render result when data is set
// Possible to show loader when data is not yet retrieved instead of null value
return this.state.event ? (
<div>Your view</div>
) : null;
}
}
Using Redux ->
class EventDetail extends React.Component {
constructor() {
super(props);
}
componentDidMount() {
// Get event id from passed prop
const id = this.props.eventId;
// Or using react-router, so you can directly link to the URL
// https://reacttraining.com/react-router/web/example/url-params
const id = this.props.match.params.eventId;
// Fire async action creator
this.props.getEvent(id);
}
render() {
// Render result when data is set
// Possible to show loader when data is not yet retrieved instead of null value
return this.props.event ? (
<div>Your view</div>
) : null;
}
}
const mapStateToProps = (state) => ({
event: state.yourReducer.whatYouNameYourStoredEvent
});
const mapDispatchToProps = (dispatch) => ({
getEvent: (id) => dispatch(getAsyncEventDetailsAction(id))
});
const EventDetailContainer = connect(mapStateToProps, mapDispatchToProps)(EventDetail);
Related
I am working on React app where the state is managed by redux. I am using actions.js file to fetch JSON data and store it directly in the store. The initial Store has just one key (data) in its obj with null as its value.
I use componentDidMount() Lifecycle to call the function which updates the store's data key with the JSON data I receive. However, whenever I load my app it gives an error because it finds the data value as null.
I get it. componentDidMount() executes after the app is loaded and the error doesn't let it execute. I tried using componentWillMount() but it also gives the same error. ( Which I use in JSX )
When I try to chanage the data's value from null to an empty obj it works for some level but after I use it's nested objects and arrays. I get error.
I wanna know what is the way around it. What should I set the vaue of inital State or should you use anyother lifecycle.
If your primary App component can't function properly unless the state has been loaded then I suggest moving the initialization logic up a level such that you only render your current component after the redux state has already been populated.
class version
class LoaderComponent extends React.Component {
componentDidMount() {
if ( ! this.props.isLoaded ) {
this.props.loadState();
}
}
render() {
if ( this.props.isLoaded ) {
return <YourCurrentComponent />;
} else {
return <Loading/>
}
}
}
export default connect(
state => ({
isLoaded: state.data === null,
}),
{loadState}
)(LoaderComponent);
Try something like this. The mapStateToProps subscribes to the store to see when the state is loaded and provides that info as an isLoaded prop. The loadState in mapDispatchToProps is whatever action creator your current componentDidMount is calling.
hooks version
export const LoaderComponent = () => {
const dispatch = useDispatch();
const isLoaded = useSelector(state => state.data === null);
useEffect(() => {
if (!isLoaded) {
dispatch(loadState());
}
}, [dispatch, isLoaded]);
if (isLoaded) {
return <YourCurrentComponent />;
} else {
return <Loading />
}
}
And of course you would remove the fetching actions from the componentDidMount of the current component.
Given an API which returns JSON data like:
["posts":
{"id":1,
"name":"example",
"date":"exampledate",
"content":"examplecontent",
"author":"exampleauthor"},
{"id":2,
..]
The length of the array is unknown.
I am fetching data via isomorphic-fetch like this:
displayPosts.getInitialProps = async function() {
const res = await fetch('.../post');
const data = await res.json();
return{
posts: data.posts
}
}
which is working (console.log.stringify(data)).
Now i want to display such posts on my displayPosts page.
Therefore i am using the following React Component.
class Posts extends React.Component {
stat = {
// here i don't know how to set the state
}
render() {
return (
// i want to render the data here
);
}
}
export default Posts;
Question: How do i set a state, so that i can neatly display every post in my displayPosts.js page with
<Posts posts={props.Posts}/>
?
class Posts extends React.Component {
state = {
posts: []
}
componentDidMount() {
this.savePosts();
}
componentDidUpdate() {
this.savePosts();
}
savePosts = () => {
if(this.props.posts){
//do any other processing here first if you like
this.setState({
posts: this.props.posts
});
}
}
You probably don't need to save the posts in state, since you could just pull them from props directly. But if you need to process or transform them somehow, it might make sense.
In either case, you just hook into the lifecycle methods to do this.
Note: This will set the state every time the component updates. Often you only want to update the state when that specific prop changes. If so, you can first compare the old and new props to see if it has changed in a way that means you want to update your state.
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 am kind of new to react and I am developing a Frontend for a REST-API.
My Frontend for a REST-API is organized in 5 Sites(Components) which are routed with react-router-dom.
Every time I enter a Site, the ComponentDidLoad dispatches an action, which in turn calls the API in my reducer.
export function getData(pURL, ...pParams){
return (dispatch) => {
axios.get(pURL, {pParams})
.then(result => {
dispatch(getDataSuccess(result.data))
})
.catch(error => {
dispatch(getDataFailure(error))
})
}
}
One of my Main sites looks as follows
class Overview extends Component {
componentDidMount(){
this.props.getData(MyURLHere);
}
render(){
return(
<div>
{this.props.hasError ? "Error while pulling data from server " : ""}
{/* isLoading comes from Store. true = API-Call in Progress */}
{this.props.isLoading ? "Loading Data from server" : this.buildTable()}
</div>
)
}
}
let mapStateToProps = state => {
hasError: state.api.hasError,
isLoading: state.api.isLoading,
companies: state.api.fetched
}
let mapDispatchToProps = {
//getData()
}
export default connect(mapStateToProps, mapDispatchToProps)(Overview);
state.api.fetched is my store-variable I store data from every API-Call in.
this.buildTable() just maps over the data and creates a simple HTML-Table
My Problem is that I got the store-state variable isLoading set to truein my initialState.
But when I move to another site and then back to this one, it automatically grabs data from state.api.fetched and tries to this.buildTable() with it, because isLoading is not true. Which ends in a "is not a function" error, because there is still old, fetched data from another site in it.
My Solution would be to always call a function when leaving a component(site) that resets my store to it's initialState
const initialStateAPI = {
isLoading: true
}
or directly set isLoading to true, in Order to avoid my site trying to use data from old fetches.
Is there a way to do so?
I hope that I provided enough information, if not please let me know.
If you want to call the function when leaving a component you can use componentWillUnmount function. Read more about React Lifecycle methods.
Say my homepage renders a feed of all posts when no user is signed in, else renders a feed of posts relevant to signed in user. With Flux I would simply have a function
getNewFeedPosts(){
this.setState({posts: []});
ajaxFetchPostsFromServer();
}
And add it as a listener to my session store. I would also have a function listening to my post store that would set the state to the new posts once they came back in response to ajaxFetchPostsFromServer being called.
What is the best practice way to do this in Redux with connectors and a provider?
Currently I have a feed container:
const mapStateToProps = (state) => ({
tweets: state.tweets,
});
const mapDispatchToProps = (dispatch) => ({
getTweets: (currUserId) => {
return dispatch(fetchAllTweets(currUserId));
}
});
const FeedContainer = connect(mapStateToProps, mapDispatchToProps)(Feed);
A feed:
class Feed extends React.Component {
constructor(props) {
super(props);
}
componentWillMount(){
this.props.getTweets();
}
render(){
const tweets = this.props.tweets;
const originalTweetIds = Object.keys(tweets);
return (
<div>
<ul>
{ originalTweetIds.map((originalTweetId) => {
return (
<li key={originalTweetId}>
{post info goes here}
</li>);
})}
</ul>
</div>
)
}
};
1) Right now I'm calling getTweets when Feed's about to mount, but if I change my session status in the other component the feed doesn't change, I suppose because it is not remounting, so how should I be doing this to make it change.
2) Also in actuality in getTweets, before I make the ajax request I want to set tweets in my store to [] to prevent tweets from the previous page (say before I logged in) from remaining on the page until the response with the proper tweets comes back. What is best practice to accomplish this?
All input is greatly appreciated :)
1) Right now I'm calling getTweets when Feed's about to mount, but if I change my session status in the other component the feed doesn't change, I suppose because it is not remounting, so how should I be doing this to make it change.
You need to call dispatch(fetchAllTweets(currUserId)) from the other component when you "change the session status". It will update your Feed component as soon as state.tweets is updated.
2) Also in actuality in getTweets, before I make the ajax request I want to set tweets in my store to [] to prevent tweets from the previous page (say before I logged in) from remaining on the page until the response with the proper tweets comes back. What is best practice to accomplish this?
You can change your mapDispatchToProps to:
const mapDispatchToProps = (dispatch) => ({
getTweets: (currUserId) => {
dispatch(clearTweets());
return dispatch(fetchAllTweets(currUserId));
}
});
Create an action creator:
const clearTweets = () => ({ type: 'CLEAR_TWEETS' });
And in the reducer that updates the tweets state you can do something like this:
switch (action.type) {
case 'CLEAR_TWEETS':
return [];
// other actions
}