This shows you the differences between two versions of the page.
Next revision Both sides next revision | |||
reactjs_and_friends [2021/10/03 16:27] sausage created |
reactjs_and_friends [2021/10/04 11:20] sausage Added in code files. |
||
---|---|---|---|
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 90: | Line 112: | ||
==== actions.js ==== | ==== actions.js ==== | ||
+ | |||
+ | <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 fetchRemoteData = (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 fetchLocalPlanetData = (dispatch) => { | ||
+ | return (dispatch) => { | ||
+ | dispatch(set_loading()); | ||
+ | setTimeout(() => { | ||
+ | dispatch( populate_store_with_data(local_planets_data) ); | ||
+ | dispatch(set_loaded()); | ||
+ | }, 2000); | ||
+ | } | ||
+ | | ||
+ | } | ||
+ | </code> | ||
There are several actions in the file. | There are several actions in the file. | ||
Line 106: | Line 198: | ||
==== 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 120: | Line 248: | ||
==== 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 { fetchRemoteData, fetchLocalPlanetData } 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(fetchRemoteData()) | ||
+ | //dispatch(fetchLocalPlanetData()) | ||
+ | }, [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 137: | Line 304: | ||
==== 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 326: | ||
==== 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 363: | ||
==== 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 424: | ||
==== 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''. |