User Tools

Site Tools


reactjs_and_friends

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
Next revision Both sides next revision
reactjs_and_friends [2021/10/03 16:27]
sausage created
reactjs_and_friends [2021/11/03 10:43]
sausage [actions.js]
Line 78: Line 78:
  
 ==== index.js ==== ==== index.js ====
 +
 +<code javascript>​
 +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'​)
 +);
 +</​code>​
  
 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. 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.
Line 91: Line 113:
 ==== actions.js ==== ==== actions.js ====
  
-There are several ​actions ​in the file.+<code javascript>​ 
 +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); 
 +    } 
 +     
 +
 +</​code>​ 
 + 
 +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.   - populate_store_with_data:​ For taking data and pushing it into the store. Used once at start up.
Line 98: Line 190:
   - set_selected_planet:​ Set when a user selects a planet.   - set_selected_planet:​ Set when a user selects a planet.
   - set_a_favourite:​ Update the list of favourites in the store.   - set_a_favourite:​ Update the list of favourites in the store.
-  - fetchRemoteData: load the planet data remotely from https://​waynejohnson.net/​planets in a cors friendly way. +  - fetchRemoteDataThunk: load the planet data remotely from https://​waynejohnson.net/​planets in a cors friendly way. 
-  - fetchLocalPlanetData: load it from a local copy, and add in an artificial delay.+  - fetchLocalPlanetDataThunk: load it from a local copy, and add in an artificial delay.
  
-The actions 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.+<WRAP center round tip 90%> 
 +For sake of ease I've thrown the thunks into the actions ​file, but ideally these would imported from their own file. 
 +</​WRAP>​ 
 + 
 + 
 +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. 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 ==== ==== reducer.js ====
 +
 +<code javascript>​
 +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;
 +</​code>​
  
 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. 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.
Line 116: Line 249:
   - favourites: A list of planet ids used to show which planets have been set as a favourite.   - 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.+As we'll see shortly, any component can use the ''​useSelector'' ​hook to access and affect any part of the state.
  
  
 ==== App.js ==== ==== App.js ====
 +
 +<code javascript>​
 +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;
 +</​code>​
  
 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). 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).
Line 125: Line 297:
 Also used is the LoadIndicator component which shows if loading is in progress. Also used is the LoadIndicator component which shows if loading is in progress.
  
-''​fetchRemoteData''​ and ''​fetchLocalPlanetData''​ are imported actions for loading the initial data. You can choose which one you'd rather use.+''​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. 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 ''​NavLink''​s. These are used instead of a ''​Nav'',​ because NavLinks allow the styling of the actively selected tab or navigation element. 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.
Line 137: Line 311:
  
 ==== list-container.js ==== ==== list-container.js ====
 +
 +<code javascript>​
 +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;​
 +</​code>​
  
 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. 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.
Line 142: Line 333:
  
 ==== list.js ==== ==== list.js ====
 +
 +<code javascript>​
 +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;
 +</​code>​
  
 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. 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.
Line 149: Line 370:
  
 ==== details.js ==== ==== details.js ====
 +
 +<code javascript>​
 +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;
 +</​code>​
  
 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. 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.
Line 158: Line 431:
  
 ==== favourites.js ==== ==== favourites.js ====
 +
 +<code javascript>​
 +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;
 +</​code>​
  
 Finally we come to the ''​favourites''​ view component. It uses two of the pieces of state: the ''​favourites''​ indexes, and the array of ''​planets''​. ​ Finally we come to the ''​favourites''​ view component. It uses two of the pieces of state: the ''​favourites''​ indexes, and the array of ''​planets''​. ​
reactjs_and_friends.txt · Last modified: 2021/11/05 01:35 by sausage