React Router, Redux and async API call - javascript

When the user clicks in a link, React Router will display a component. The data this component will show came from an endpoint, so I'm wondering what's the best practice.
I've created a function called fetchData which uses fetch to perform a GET in an endpoint and then returns a promise. Once this promise is resolved, I would like to dispatch a Redux action to update the state.
I managed to do this with redux-thunk, but I would like to implement this without adding more libraries.
I'm trying to follow the 'container/presentational' idea and I'm using stateless functional components in React.
In a general overview, this is what I'm doing:
index.js
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
app.js
const App = () => (
<div>
<BrowserRouter>
<div>
<Header />
<Switch>
<Route exact path="/" component={Main} />
<Route exact path="/data-list" component={DataListContainer} />
</Switch>
</div>
</BrowserRouter>
</div>
)
dataListContainer.js
const mapStateToProps = (state) => {
return { data: state.data }
}
const mapDispatchToProps = (dispatch) => {
return { }
}
const DataListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(DataList)
dataList.js
const DataList = (props) => {
const rows = props.data.map(data => {
return <Data data={data} />
})
return (
<div>
{rows}
</div>
)
}
And the Data component simply displays the data. I'm wondering where I should add the call to the function fetchData and where I should solve the returned promise. I imagine I will need to dispatch an action after the promise is resolved, but not sure where is the best place to do this.
Other question is: I would like to fetch the data only once, I mean, only when the user clicks the /data-list link. If it comes back to main page and then goes again to data-list, I would like to not call the endpoint again. Is there any call once feature hidden in React Route implementation?

Change dataListContainer.js to be a stateful React component, this is ok because it's the container! Don't take the code for exact, it's to just to give an idea.
import { fetchData, DataList, store } '......'
class DataListContainer extends React.Component {
componentDidMount() {
if (!this.props.data) {
store.dispatch(fetchData())
}
}
render() {
return (<DataList data={this.props.data}/>);
}
}
const mapStateToProps = (state) => {
return { data: state.data }
}
export default connect(
mapStateToProps
)(DataListContainer)

I don't think this is possible in the routes and views. When you switch pages with React Router, the component unmounts. All the data of the component is also cleared. (Believe me, this is something you want. Otherwise you might get some serieus memory issues that let your browser crash). Also, your view is only responsible for displaying stuff and should not do things with the data it receives.
Take a look for some implementation in your store. For example store the received data from the API in the store object. The next time someone is calling the fetchData function in the store, serve the stored data instead of make a new request. The store never unmounts so it can hold data in memory. Keep in mind that the user will only receive new data if the reload the entire page. So a 'hard refresh' but might be useful..
Another thing you can do is asking yourself why you don't want to call that endpoint multiple times? Is the data set to large to receive? In that case use pagination and serve it in pieces.

Related

Sharing data with child components without props

I'm sharing data with child components like the code below.
const App = () => {
const [appData, setAppData] = useState({});
const dealId = new URLSearchParams(window.location.search).get('dealId');
useEffect(() => {
/* API Call here */,
(response) => {
setAppData(
Object.entries(response).reduce((previousValue, currentValue) => {
return {
...previousValue,
[currentValue[0]]: currentValue[1].data(),
};
}, {})
);
}
);
}
}, [dealId]);
return (
<React.Fragment>
<CSSBaseline />
<NavAppBar dealId={dealId} user={appData.user} />
<Panel>
<Contact
contact={appData.contact}
contactFields={appData.contactFields}
/>
<Event event={appData.event} />
<Accommodation
packageItemFields={appData.packageItemFields}
packageItems={appData.packageItems}
hotels={appData.hotels}
/>
<Course courses={appData.courses} event={appData.event} />
<Transportation transportationFields={appData.transportationFields} />
</Panel>
</React.Fragment>
);
};
export default App;
This feels like not a good pattern since I need to pass the same data more than one time to different components. Is there any way to improve on this? Using context would be more suitable?
Rule of thumb that will help you in deciding when is the right time to add state management like Context api or Redux that I use is when I see myself prop drilling heavy now I then turn to using Context Api.
Ask yourself the following questions below before even deciding between Context or Redux
When should I use Context Api?
Ans: If you are using Redux only to avoid passing props down to deeply nested components, then you could replace Redux with the Context API
So when should I use Redux?
Ans: Redux is a state container, handling your application's logic outside of your components, putting your entire app state in one container, using Redux DevTools to track when, where, why, and how your application's state changed, or using plugins such as Redux Saga,Redux Persist, etc.
In This case you can use Redux.
So now if you keep this ideas in mind then you will choose wisely

using context with an api call for the root level component

i am new to use the react context, but i think how the redux works like, but not getting the proper way.
so i have a component where i will get data from api once i get the data i need to show a loader once data gets load the component
but how i set the value to context and use it.
see the below snippet
class App extends Component{
componentDidMount(){
this._fetchData()
}
_fetchData = async () => {
const response = await fetch("http://www.example.com", {method: "GET"})
const data = await response.json()
// how can i use context and set the value
}
// how should i use a provider to the router so basically i need to get the data every where in the route
render(){
return(
<Router>
{ Object.keys(context.data).length > 0 ?
<Layout>
<Switch>
<Route path="/sample" exact component={SampleHomePage} />
<Route path="/sample/profile" component={SampleProfile} />
<Route path="/sample/about" component={SampleAbout}/>
</Switch>
</Layout>
: <p>Data Loading </p>}
</Router>
)
}
}
// profile.js
import React from 'react';
const profile = () => {
//how to get the contex value here
}
export default profile
Look at this codesandbox and check how i worked with context.
https://codesandbox.io/s/2p0koqr81p
The key is that you need an extra component that will work as the context.
In this case i've just saved the data from the fetch call into the context.
So you have to provide the updated data to the context so then you can consume it in any component that you want.
I agree with #Joru that maybe is unnessesary to use context in your use case, but im trying to answer the question.

React Reuse Fetch JSON data throughout components

Hi there I am newbie to React and learning about reusable functions throughout my project.
I would like to fetch my JSON data but not have to call it everytime in my component.
App.js
import React, { Component } from 'react';
import { Route, NavLink, HashRouter } from "react-router-dom";
import logo from '../assets/logo.png';
import './app.css';
import About from "../about/about";
import Services from "../services/services";
import Testimonials from "../testimonials/testimonials";
import Contact from "../contact/contact";
class App extends Component {
constructor(props) {
super(props);
this.state = {
items : []
};
}
componentDidMount(){
this.getItems();
}
getItems(){
fetch('./data/data_arr.js')
.then(results => results.json())
.then(results => this.setState({'items': results}));
}
render() {
return (
<HashRouter>
<div className="container">
<div className="header">
<div className="App-logo"><NavLink exact to="/"><img src={logo} alt="logo" /></NavLink></div>
<nav className="Nav-Desktop">
{this.state.items.map((item, index) => (
<div key={index}>
{
item.links.map((link, i) => (
<NavLink key={i} exact to={link.url}>{link.text}</NavLink>
))}
</div>
))}
{
this.state.items.map((item, index) => {
return <div key={index}></i><strong> {item.mainContact.phone}</strong></div>
})
}
</nav>
</div>
<main className="content">
<Route exact path="/" component={About}/>
<Route path="/services" component={Services}/>
<Route path="/testimonials" component={Testimonials}/>
<Route path="/contact" component={Contact}/>
</main>
{this.state.items.map((item, index) => {
return <footer key={index}>© Copyright {item.title} {(new Date().getFullYear())}</footer>
})
}
</div>
</HashRouter>
);
}
}
export default App;
I am successfully mapping my data and displaying it, but I have other files that include this snippet
constructor(props) {
super(props);
this.state = {
items : []
};
}
componentDidMount(){
this.getItems();
}
getItems(){
fetch('./data/data_arr.js')
.then(results => results.json())
.then(results => this.setState({'items': results}));
}
I have tried exporting the getItems() like so in a helper.js file and importing the file import { getItems } from '../helpers/helpers'; however the code did not work properly and got stuck at Unhandled Rejection (TypeError): Cannot read property 'setState' of undefined
export function getItems() {
fetch('./data/data_arr.js')
.then(results => results.json())
.then(results => this.setState({'items': results}));
}
If anyone can give me pointers as to the error / right way to go about this that would be helpful. Cheers
Two things you need to know when you want to reuse the data instead of calling fetch again and again
Do fetch call in top most component i.e., parent component and pass down data to all the children, children to children components but, do remember this will be hectic for you. This approach is good when you are building small application which will be like max to max 50 components. But when your application grows big this is not a recommended way of reusing the data across components.
Use Redux state management library for data reusability across components. This acts like a centralised store for your application. This is used mostly in every React app these days. With Redux you can make an action call in parent component and that action will actually fetch the data and pass it to the reducer. So now reducer will set the data in Redux store. Now the data is accessible in every component by getting the data from Redux store using state.get. So you can call redux action like this.props.getItems(); wherever you need the data in the component and the component mapStateToProps will make that data available to your component as props
How to get the data from Redux store?
Define a function mapStateToProps in component and get the data from Redux store using state.get and return it in the function. Pass the mapStateToProps to connect method. You will be connecting your component to Redux HOC component called connect. This connect method accepts actions and Redux state and make them available to the component as props.
Regarding your issue
Unhandled Rejection (TypeError): Cannot read property 'setState' of undefined
The reason you get this issue because this isn’t available inside the exported getItems function.
What you need to do to fix the issue is pass this to getItems function as a parameter
Note: Here this is a current context
import { getItems } from '../helpers/helpers';
componentDidMount(){
getItems(this);
}
helpers.js:
the below function is treated as a normal JavaScript function. This function no wr belongs the component to bind it in component constructor. But in order to play with the state of the component you need to pass the component context this. But this is old way, now a days we all use Redux to avoid these old concepts
export function getItems(this) {
fetch('./data/data_arr.js')
.then(results => results.json())
.then(results => this.setState({'items': results}));
}
This way you can reuse this function and do the setState.
But do remember these solutions will make complex when your application grows big in future. So I would argue you to go with Redux state management library from now to avoid hectic migration in future :)
Excuse me if there are any typo errors because I am answering from my mobile.
You need to pass the context to your helper function. Considering you're still importing the function from your helper, on your componentDidMount, it can be like:
componentDidMount(){
getItems.call(this);
}
So good job with the fetching, there are a lot of paths you can take with sharing data across components, the two main ones you can look into are:
Parent state holds the data, passes it through props
A Store (Mobx, Redux) holds the data, and you inject it into your components as needed
I'm not going to give you the tutorial on how to do them all but I'm assuming the first option will be best for you right now.
If your components are using the same data, they should theoretically have the same parent component in a structure similar to this:
Page Component (Overarching component, lays out the page structure)
Header
Component 1
Component 2
Footer
So you'll do this call on the mount of Page component and pass this.state.items as a prop to the other components that need it... Stores have a similar process they just save the double handling of props :)
Since you're accessing/modifying state in your getItems() method, you should bind it in your constructor like this:
constructor() {
super(props);
this.state = {
items : []
};
this.getItems = this.getItems.bind(this);
}
or you can simply declare it with arrow function so you don't have to bind it:
getItems = () => {
// some codes here
}
Another thing, the only ones who can access/modify the state of a compoenent is its own methods. If you want to use it outside of itself, import those components inside your component as children then pass your getItems() method as prop.
render() {
return (
<HashRouter>
<div className="container">
// some other codes
<ChildComponent getItems={this.getItems} />
</div>
</HashRouter>
);
}

Where do I fetch initial data from server in a React Redux app?

I've started learning React / Redux and stumbled on something that is probably a very basic question. Below are snippets from my app with some code removed for simplicity sake.
My state is described by an array of sites which is empty by default. Later reducer will have LOAD_SITES action to load a different set of sites whenever user paginates to a different page but for now it's doing nothing. React starts by rendering PublishedSitesPage which then renders PublishedSitesBox which then loops over data and renders individual sites.
What I want to do is have it render everything with the default empty array and meanwhile initiate a "load sites from server" promise and once it resolves, dispatch LOAD_SITES action. What is the best way to make this call? I was thinking about either constructor of PublishedSitesBox or perhaps componentDidMount. But not sure if this would work - my concern is that I'll create an endless loop this way that will keep re-rendering. I guess I could prevent this endless loop in some way by having some other state param along the lines of "haveRequestedInitialData". Another idea I had is to simply make this promise right after doing ReactDOM.render(). What is the best and cleanest way to do this?
export default function sites(state = [], action) {
switch (action.type) {
default:
return state;
}
}
...
const publishedSitesPageReducer = combineReducers({
sites
});
ReactDOM.render(
<Provider store={createStore(publishedSitesPageReducer)}>
<PublishedSitesPage />
</Provider>,
this.$view.find('.js-published-sites-result-target')[0]
);
...
export default function PublishedSitesPage() {
return (
<PublishedSitesBox/>
);
}
...
function mapStateToProps(state) {
return { sites: state.sites };
}
const PublishedSitesBox = connect(mapStateToProps)(({sites}) => {
// render sites
});
There's no reason for this data load logic to touch your React components at all. What you want here is for the return of the promise to dispatch an action to your reducers, which make the appropriate changes to the store, which then causes the React components to re-render as appropriate.
(It doesn't matter whether you kick off the async call before or after you call ReactDOM.render; the promise will work either way)
Something like this:
var store = createStore(publishedSitesPageReducer);
someAsyncCall().then(function(response) {
store.dispatch(someActionCreator(response));
});
ReactDOM.render(
<Provider store={store}>
<PublishedSitesPage />
</Provider>,
this.$view.find('.js-published-sites-result-target')[0]
);
Your React components are consumers of your store, but there's no rule that they need to be the ONLY consumers of your store.
There is a clear example here on how to do it : Facebook Tutorial
As for the endless loop, Once your array is no longer empty , Clear the Interval.
This should prevent the loop.

Redux and React - Keeping component props in sync with state via #connect

I have an application set up using React and Redux such that I have a root application component wrapped around a provider (which is passing the store down so that my child components can access state and dispatch) like so:
<Provider store={Store}>
<RootComponent />
</Provider>
My RootComponent essentially uses a JSON file to render Template components that get passed a key and title (
<Application children={children} />
Within one of the children, I've hooked up the state of the application to the Component using the #connect decorator and I'm firing off a dispatcher upon construction to update my 'content' prop like so:
#connect(state => ({content: state.content}))
export default class DynamicText extends Component {
constructor(props) {
super(props);
var Content = require(`./jsonContent`);
props.dispatch(loadContent(Content[props.title].elements));
}
**actions.js**
export function loadContent(content) {
return {
type: 'LOAD_CONTENT',
content
};
}
**reducers.js**
const initialState = {
route: null
};
export default function routeReducer(state = initialState, action) {
switch (action.type) {
case LOAD_CONTENT:
return Object.assign({}, state, {
content: action.content
});
default:
return state;
}
The issue I'm having is that when booting up my application, the 'content' prop is undefined - upon page refresh however, it populates with the updated 'content' data from the JSON file. I think the issue may be occurring because initially the state.content referenced in my #connect decorator is undefined, however I was hoping that the immediate dispatch on the constructor would solve this issue.
Does anyone have experience or recommendations on how I can get this working without a refresh? Is #connect really the best option here or are there alternatives?
You need to hydrate your store before passing it down to Provider providing an initial state to it so that when your app "boots" your store already has the data available.
Using connect is the right way to go after that.
If you're running a server side rendered application you can put your json contents in a property attached to window on first request then empty it after store has been hydrated.
Something along the lines of window.__BOOTSTRAP__.
store = createStore(appReducers, { content: yourJsonFileContents });
<Provider store={store}>
<RootComponent />
</Provider>

Categories

Resources