import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import reducer from './reducers/reducer';
const store = createStore(reducer, applyMiddleware(thunk));
ReactDOM.render(
,
document.getElementById('root')
);
This is the root of our application. Stores and middleware loading are provided by Redux. A middleware will be used to add the ability to run a function from within an action. Normally an action can only work with a standard object. Using middleware, will allow us asynchronously load in data remotely using ''fetch'' and then pass the data into the store.
The middleware itself will be Redux-Thunk.
React-Redux is the bridge between the Redux and React and the Provider component is used to wrap the entire application. This gives any component or view the ability to access any part of the state cleanly and easily.
Our reducer (which we will cover later) is used to affect the data in the store containing our state. The store is created by passing in the reducer and the thunk. The store is passed into the Provider, for all components to access at will.
Finally, the app is rendered into the root element on our page.
==== actions.js ====
import local_planets_data from '../reducers/planets.json';
export const populate_store_with_data = (data) => {
return {
type: 'LOAD_PLANETS_INTO_STORE',
data: data
}
}
export const set_loading = () => {
return {
type: 'SET_LOADING'
}
}
export const set_loaded = () => {
return {
type: 'SET_LOADED'
}
}
export const set_selected_planet = (id) => {
return {
type: 'SET_SELECTED_PLANET',
id: id
}
}
export const set_a_favourite = (data) => {
return {
type: 'SET_A_FAVOURITE',
data: data
}
}
export const fetchRemoteDataThunk = (dispatch) => {
return (dispatch) => {
dispatch(set_loading());
return fetch('https://waynejohnson.net/planets',
{
mode: 'cors',
method: 'GET',
headers: {
'Content-Type': 'text/plain',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(json => {
dispatch( populate_store_with_data(json) )
}).then( () => {
dispatch(set_loaded());
});
}
}
export const fetchLocalPlanetDataThunk = (dispatch) => {
return (dispatch) => {
dispatch(set_loading());
setTimeout(() => {
dispatch( populate_store_with_data(local_planets_data) );
dispatch(set_loaded());
}, 2000);
}
}
There are several Action Creators in the file.
- populate_store_with_data: For taking data and pushing it into the store. Used once at start up.
- set_loading: Indicate if data is currently loading.
- set_loaded: Indicate that data loading has finished.
- set_selected_planet: Set when a user selects a planet.
- set_a_favourite: Update the list of favourites in the store.
- fetchRemoteDataThunk: load the planet data remotely from https://waynejohnson.net/planets in a cors friendly way.
- fetchLocalPlanetDataThunk: load it from a local copy, and add in an artificial delay.
const initialState = {
loading: false,
planets: [],
selectedPlanet: null,
favourites: []
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_PLANETS_INTO_STORE':
return { ...state, planets: action.data }
case 'SET_LOADING':
return { ...state, loading: true }
case 'SET_LOADED':
return { ...state, loading: false }
case 'SET_SELECTED_PLANET':
return { ...state, selectedPlanet: action.id }
case 'SET_A_FAVOURITE':
if (action.data.checked){ //add a favourite
return {
...state, favourites: [...state.favourites, action.data.id]
}
} else { //remove a favourite
return {
...state, favourites: [ ...state.favourites.filter(ids => ids !== action.data.id) ]
}
}
default:
return state
}
}
export default reducer;
Not much to say on the reducer file for the reducer itself, as it's pretty standard fare: responding to the dispatched action, and changing the data in the store.
But on the initialState that has been set up there, note the state has been broken up into a number of pieces:
- loading: This is the indicator that can be used to show if data is currently loading or not.
- planets: The array that holds the list of planet objects. This is the main data for the application.
- selectedPlanet: An id that is kept to indicate which is the currently selected planet.
- favourites: A list of planet ids used to show which planets have been set as a favourite.
As we'll see shortly, any component can use the ''useSelector'' hook to access and affect any part of the state.
==== App.js ====
import {BrowserRouter as Router, Route, NavLink, Redirect} from 'react-router-dom';
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchRemoteDataThunk, fetchLocalPlanetDataThunk } from './actions/actions';
import ListContainer from './views/list-container.js';
import Favourites from './views/favourites.js';
import LoadIndicator from './components/loadindicator.js';
import './App.css';
const App = () => {
const dispatch = useDispatch();
useEffect( () => {
dispatch(fetchRemoteDataThunk())
//dispatch(fetchLocalPlanetDataThunk())
}, [dispatch] );
return (
Planet Viewer
List
Favourites
);
}
export default App;
This is the main app component itself. It uses two view components: ''ListContainer'' and ''Favourites'' and these act as two tabbed views to make up a small Single Page Application (SPA).
Also used is the LoadIndicator component which shows if loading is in progress.
''fetchRemoteDataThunk'' and ''fetchLocalPlanetDataThunk'' are imported actions for loading the initial data. You can choose which one you'd rather use.
The useEffect hook is going to be used as the trigger to load data into the application when it first starts. Providing an empty array to useEffect is the equivalent to the old-school lifecycle method ''componentDidMount''. Therefore, the loading will only occur on first load and will then push the data into the store.
import List from '../components/list.js';
import Details from '../components/details.js';
const ListContainer = () => {
return (
);
}
export default ListContainer;
This is only a dumb component that groups the List and Details components. I'll dig into those two components first. We'll look at favourites after that.
==== list.js ====
import { useSelector, useDispatch } from 'react-redux';
import { set_selected_planet } from '../actions/actions';
import PlanetItem from './planet-item.js';
const List = () => {
const dispatch = useDispatch();
const planets = useSelector(state => state.planets);
const selectedPlanet = useSelector(state => state.selectedPlanet);
return (
{
planets.map( planet => (
dispatch(set_selected_planet(planet.id)) }
active={selectedPlanet === planet.id }
>
))
}
);
}
export default List;
This component uses two useSelector hooks. One to connect to the ''planets'' array state in the store, and one to connect to the ''selectedPlanet'' state. For each planet, a ''PlanetItem'' component is rendered.
When a PlanetItem is clicked, the useDispatch hook is used to update the ''selectedPlanet'' state in the store.
==== details.js ====
import { useSelector, useDispatch } from 'react-redux';
import { set_a_favourite } from '../actions/actions';
const Details = (props) => {
const dispatch = useDispatch();
const planets = useSelector(state => state.planets);
const selectedPlanet = useSelector(state => state.selectedPlanet);
const favourites = useSelector(state => state.favourites);
const planet = planets.find(p => p.id === selectedPlanet);
const isFavourite = (favourites.filter(i=>i === selectedPlanet).length > 0);
return (
{ planet ?
<>
{ planet.name }
Environment: { planet.environment}
Resources:
{
planet.resources.map( (resource, index) => (
- { resource }
))
}
{
dispatch(set_a_favourite( {id:selectedPlanet, checked: control.currentTarget.checked}))}
}/>
>
:
}
);
}
export default Details;
This is the second component in the pair under the list-component view. It's job is to respond to the change in the selectedPlanet state. But to the user, it appears to respond to the selection click from the list.
Details uses three out of the four pieces of state by using three useSelector hooks. It uses ''selectedPlanet'' for the selected index, and the list of ''planets'' to drill into the data to display.
There is also a ''Favourite'' checkbox here which is both set based on the ''favourites'' indexes, and also dispatches the ''set_a_favourite'' action to update the ''favourites'' array of indexes. This is a special action that will update the state by either adding or removing an index based on the ''checked'' value.
==== favourites.js ====
import { set_selected_planet } from '../actions/actions';
import { useSelector, useDispatch } from 'react-redux';
const Favourites = () => {
const dispatch = useDispatch();
const favourites = useSelector(state => state.favourites);
const planets = useSelector(state => state.planets);
const favouritePlanets = planets.filter(p => favourites.includes(p.id))
return (
{ favouritePlanets.length > 0 ?
favouritePlanets.map( planet => (
dispatch(set_selected_planet(planet.id))
}>{` \u2605 ${planet.name} `}
))
: No favourites yet.
}
);
}
export default Favourites;
Finally we come to the ''favourites'' view component. It uses two of the pieces of state: the ''favourites'' indexes, and the array of ''planets''.
These are displayed as a list of favourite planets. When clicking on any in this list, the ''set_selected_planet'' action is dispatched to change the state in the store, and the selected planet will be different when navigating back to the list.
===== Wrap up =====
I hope this is helpful. There are many real world concepts covered here, but not all. There has been no analysis of unnecessary renders. Don't forget to look into memo, and test your components to ensure that only the state required is changed so that only the required components react to those changes.
You can see that my favourite friends of ReactJS have matured and are so much easier to reason with. Hooks have removed the need to use ''connect'' and mapping in the components which is a great relief.
There has been no need to implement any local state in any of the components. All have been driven by the useSelector hook back from the main application state.
Finally a big thanks to my friends Andrew and Adam for reviewing the content of this article for me.
===== References and further reading =====
- https://www.code-boost.com/create-react-app
- https://medium.com/@mendes.develop/introduction-on-react-redux-using-hooks-useselector-usedispatch-ef843f1c2561
- https://stackoverflow.com/questions/59304283/error-too-many-re-renders-react-limits-the-number-of-renders-to-prevent-an-in
- https://medium.com/swlh/using-the-redux-thunk-to-dispatch-async-actions-with-react-8df0addf34ce
- https://www.freecodecamp.org/news/a-complete-beginners-guide-to-react-router-include-router-hooks/
- https://reactgo.com/redux-fetch-data-api/
- https://blog.container-solutions.com/a-guide-to-solving-those-mystifying-cors-issues
- https://javascript.info/fetch-crossorigin
- https://namespaceit.com/blog/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook
- https://developer.mozilla.org/en-US/docs/Web/API/fetch
- https://www.vhudyma-blog.eu/yarn-vs-npm-2020/