User Tools

Site Tools


reactjs_and_friends

ReactJS and Friends Refresher

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 sure do make a darned good Javascript framework.

The name React (in my option) comes from the fact that the rendering reacts 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:

  1. Uses Redux for storing state, and providing middleware to allow running a function within an action.
  2. Uses React-Redux to help manage data in the Redux store from any of the components.
  3. Uses Redux-Thunk which is perfect for fetching remote data during an action.
  4. Uses React-Router for switching between view components, and thus, providing a Single Page Application experience.
  5. Uses the useEffect hook to manage when to fetch the first time data.
  6. Uses the useSelector hook to subscribe components to parts of the state.
  7. Uses the useDispatch hook to trigger changes to the state, and in turn, re-render parts of the application.
  8. Uses Actions and a Reducer as per standard issue to alter the state.

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.

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:

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(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  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.

  1. populate_store_with_data: For taking data and pushing it into the store. Used once at start up.
  2. set_loading: Indicate if data is currently loading.
  3. set_loaded: Indicate that data loading has finished.
  4. set_selected_planet: Set when a user selects a planet.
  5. set_a_favourite: Update the list of favourites in the store.
  6. fetchRemoteDataThunk: load the planet data remotely from https://waynejohnson.net/planets in a cors friendly way.
  7. fetchLocalPlanetDataThunk: load it from a local copy, and add in an artificial delay.

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:

  1. loading: This is the indicator that can be used to show if data is currently loading or not.
  2. planets: The array that holds the list of planet objects. This is the main data for the application.
  3. selectedPlanet: An id that is kept to indicate which is the currently selected planet.
  4. 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 (
    <div className="App">
      <h1>Planet Viewer</h1>
      <Router>
        <div className="tabs">
          <LoadIndicator></LoadIndicator>
          <NavLink to="/list" activeClassName="active">List</NavLink>
          <NavLink to="/favourites" activeClassName="active">Favourites</NavLink>  
        </div>
        <Route path="/list" component={ListContainer}></Route>
        <Route path="/favourites" component={Favourites}></Route>
        <Redirect to="/list" />
      </Router>
    </div>
  );
}
 
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.

However, do notice that I have passed in dispatch in the array for useEffect. This still behaves like componentDidMount but will remove an ugly lint warning.

The Router is added here, which contains NavLinks. 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 (
        <div className="list-container">
            <List></List>
            <Details></Details>
        </div>
    );
}
 
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 (
        <div className="list">
            {
                planets.map( planet => (
                    <PlanetItem key={planet.id} planet={planet} onClick={ () => 
                        dispatch(set_selected_planet(planet.id)) } 
                        active={selectedPlanet === planet.id }
                        >
                    </PlanetItem>
                ))
            }   
        </div>
    );
}
 
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 (
 
            <div className="details">
                { planet ? 
                    <>
                        <div>
                            <h2>{ planet.name }</h2>
                            <p>Environment: <span className="environment">{ planet.environment}</span></p>
                            <p>Resources:</p>
                            <ul>
                                { 
                                    planet.resources.map( (resource, index) => (
                                        <li key={index} className="resource">{ resource }</li>
                                    )) 
                                }
                            </ul>
                        </div>
                        <div className="select-favourite">
                            <div className="control">
                                <input id="fav-check" type="checkbox" checked={isFavourite} onChange={(control) => {
                                    dispatch(set_a_favourite( {id:selectedPlanet, checked: control.currentTarget.checked}))} 
                                }/>
                                <label htmlFor="fav-check">Favourite</label>
                            </div>
                        </div>
                    </>
                : <span></span>
                }
            </div>
 
    );
}
 
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 (
        <div className="favourites">
            { favouritePlanets.length > 0 ?
                favouritePlanets.map( planet => (
                    <div key={planet.id} className="planet-item" onClick={ () => 
                        dispatch(set_selected_planet(planet.id)) 
                        }>{` \u2605 ${planet.name} `}
                    </div>
                ))
                : <span>No favourites yet.</span>
            }   
        </div>
    );
}
 
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.

References and further reading

reactjs_and_friends.txt ยท Last modified: 2021/10/20 04:41 by sausage