====== ReactJS and Friends Refresher ====== {{:react:react-and-friends.png?direct&400 |}} Whenever I take a spell from ReactJS it sometimes takes a little bit of time to warm up again. You might be the same. Hopefully this article will serve as a good reminder for when you need to stretch your legs again. Facebook are not known for the fostering of healthy relationships and interactions, but they make a darned good Javascript framework. The name React (in my option) comes from the fact that the need to render UI is a reaction to the changes in the data that the components themselves are bound to. But without some extra libraries, the React framework is of limited real-world use. My favourite friends of React are: React-Redux, React-Router, Redux-Thunk and Hooks. For this refresher, I'm going to present a simple, but reasonably complete application that: - Uses Redux for storing state, and providing middleware to allow running a function within an action. - Uses React-Redux to help manage data in the Redux store from any of the components. - Uses Redux-Thunk which is perfect for fetching remote data during an action. - Uses React-Router for switching between view components, and thus, providing a Single Page Application experience. - Uses the useEffect hook to manage when to fetch the first time data. - Uses the useSelector hook to subscribe components to parts of the state. - Uses the useDispatch hook to trigger changes to the state, and in turn, re-render parts of the application. - Uses Actions and a Reducer as per standard issue to alter the state. ===== Recommended Tools ===== Node is required for setting up a React application, but I recommend the use of ''nvm'' instead to be able to manage multiple versions of NodeJS/npm. You can download nvm from: https://github.com/coreybutler/nvm-windows/releases I also recommend ''yarn'' over npm. Without getting in the wars too much, the parallel download of packages is a noticeable feature. But if you are used to npm and prefer that, you can get along with this refresher fine. You can get yarn with: npm install --global yarn ===== Setting Creating a default React App ===== Create an empty folder called ''PlanetUI''. Our Single Page Application is going to be used to view the available planets in the list, allow us to get important information about each planet, and set them as favourites if you like. Additionally, you can switch to the Favourites view to show all the planets that have been set as a favourite. {{ :react:react-app-screenshot.png?direct|}} A trivial application of course, but it demonstrates many real world elements required for a React UI application. Open the empty PlanetUI folder using VSCode. And open the Terminal pane within VSCode for running commands. In order to create the actual basic React application structure, I like to use ''create-react-app''. This is due to its simplicity, and the fact that all the webpack business is handled behind the scenes for you. Rather than re-invent the wheel, follow this really clear guide on how to use ''create-react-app'': https://www.code-boost.com/create-react-app Once you are done, head back to here and we can talk through the files of the PlanetUI application. ===== The Design ===== Even small applications with a number of components can benefit from a little bit of design. This helps to get your head clearly in the game and plan out what you will need in terms of components. Also to start thinking about the kind of state you might need. This is my little mockup using Inkscape: {{ :react:react-app-design.png?direct |}} ===== The application ===== Browse or clone the repo at: https://github.com/sausagejohnson/PlanetUI Copy in the files from the repo into your newly created React app. To get all the extra required libraries: Redux, React-Redux, Redux-Thunk and React-Router, you'll need to add them with: yarn add redux yarn add react-redux yarn add redux-thunk yarn add react-router You can build and serve the UI anytime using ''yarn start''. ===== Tour of the application ===== As this is not a tutorial as such, but rather a tour, we won't work through building up an app. Instead, I will just show each file and point out items of note. ==== App.css ==== Just some customised syles for the PlanetUI app to add some aesthetics. I won't list it out as there's nothing specific to say about the styling itself. ==== index.js ==== 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. For sake of ease I've thrown the thunks into the actions file, but ideally these would imported from their own file. The Action Creators are written as simple functions that can pass in data. This makes it easy for a component to import an action which can be dispatched and data passed along too. A note on the two fetch data actions above: I've written them with Promise chaining with ''then'' rather than using the ''await'' operator on the promises. I'm still on the fence here, but you can re-write them accordingly to taste. ==== reducer.js ==== 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. Notice that I have passed in ''dispatch'' into the array for useEffect. This is to remove an ugly warning. Because the array with ''dispatch'' is unchanging, useEffect still behaves like ''componentDidMount''. The ''Router'' is added here, which contains ''NavLink''s. These are used instead of a ''Nav'', because NavLinks allow the styling of the actively selected tab or navigation element. The Route paths are added here too. Each binds to a specific component to load on navigation which is either ''list'' or ''favourites''. The default route is ''list''. Now to the two view components. ==== list-container.js ==== 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/