The log of my 2nd Round of #100DaysOfCode challenge. Started on [July 19, Friday, 2019].
In my first life I was a COBOl and Mainframe Software Developer. Being a Product Owner and Requirements engineer in the last decade, I did not code anymore and found out that I was missing it like hell. I made a plan to get back into coding frequently and am now on a mission learning JavaScript and ReactJS. I started my personal #100DaysOfCode challenge on March 24th 2019 but only logged my daily progress via Twitter (@markkrake). I'm starting the 2nd round an will log my progress here.
- Setup my git repository to log my 2nd round progress of my #100DaysOfCode challenge
cd
into yourcoding-repository
npm init -y
npm install -D eslint eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard
- create
.estlintrc.js
with ```module.exports = {"extends":"standard"}; - In VSCode make sure that ESLint is installed, id not then install it and restart VSCode
- I started a new git project locally and decided to also push this to GitHub to allow to track my progress on that too. Here is how I did this https://help.github.com/en/articles/adding-an-existing-project-to-github-using-the-command-line
- Learning JSX in theory and practical coding. Started with building components.
- checked out Sematic UI https://semantic-ui.com/
- a Free and Open Source development development framework that helps create beautiful, responsive layouts using human-friendly HTML
- just copied the semantic.min.css link from https://cdnjs.com/libraries/semantic-ui and added to index.html as stylesheet
- Using it for my component styling when lerning ReactJS
- Creating a ReactJS component for comments with a naive approach (all comments in 1 JSX div)
- Using Faker.js to generate massive amounts of realistic fake data in Node.js and the browser during the development phase of my project (https://github.com/marak/Faker.js/).
npm install --save faker
- After install of Faker.js the local websever did not want to start anymore. I deleted the node_modules folder of my project and did a fresh
npm install
. After that usednpm start
to try again, and worked.
- first time used
git commit --amend
- allows you to adjust the last commit message directly via editor
- refactoring JSX content and extracting to an own component
CommentDetails.js
- coding convention: Components are written with UpperCamelCase
- joined component with main App via
export default
andimport
- using the component as JSX tag `` in render method of the main App
- component hierarchy, parent component, child components or children
- props = properties
- props: system for passing data from a parent component to a child component
- goal is to customize or configure a child component
- provide information from parent to child (via JSX property) e.g.
<CommentDetails date="Today 6:05PM" />
- consume infomation in child (via props argument object) e.g.
{props.date}
- Runtime type checking for React props and similar objects.
- during debugging of metasfresh-webui-frontend I came past an eslint warning
... is missing in props validation
- documentation here https://www.npmjs.com/package/prop-types
- component nesting: a component can be shown inside of another
- component reusability: components shall be easily reusable through out an application
- component configuration: a component shall be configurable when it's created
- wrapped the comments component into an approval component via jsx
- used
props.children
to consume the nested component
- function based: good for simple (JSX) content
- class based: good for just about everything else (including event handling)
- easier code organization
- can use
state
means easier to handle user input - understands lifecycle events, means easier to do things when the app first starts
- further digging into the metasfresh webui frontend code
- getting a better understanding about how things work after my first week of ReactJS learning
- decided to start the developer documentation of metasfresh webui frontend as part of my learning time, I'm thinking about using JSDocs or Gatsby for this
- read about Storybook and how is can help developing encapsulated and reusable components
- wrote a story for a small component in metasfresh webui project and tested different configurations in isolation
- the styling is not compoinent based in metasfresh, so the component is not looking good in Storybook runner. I will see how to change this.
- created a new ReactJS challenge project to learn more about class based components, state and event handling
- adding sematic ui to the project
- checked out and used the Geolocation API which is included in most browsers
- documentation can be found here https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API
- Rules of class base components
- must be a javascript class
- must extend subclass React.Component
- muste define a
render
method that returns some amount of jsx
- Rules of State
- only usable with class components (can be used with functional components using the
hooks
system) - don't confuse props with state
State
is a javascipt object that contains data relevant to a component- updating
state
on a component causes the component to (almost) instantly rerender State
must be initialized when a component is created- Never do direct assignment to state (besides initialization), always use setState
- don't use heavy calculations in render method (is called a lot and shall remain performant)
- only usable with class components (can be used with functional components using the
- using if statements in render() method to decide what content shall be returned
- Lifecycle methods are functions that are called discrete or distinct during the lifecycle time
- constructor
- optional
- defined and called when new component is created
- good place to do one-time setup e.g. state initialization
- for cleaner code, it is not recommended to add data loading to the constructor method. leave this for componentDidMount method
- render
- not optional
- content is now visible on screen
- important: avoid doing anything besides returning JSX
- componentDidMount
- optional
- is called immediatly after a component shows up first time on the screen/ browser
- good place to do data loading
- componentDidUpdate
- optional
- is called automatically every time our component is updated
- every time the render method is called, the jsx will be returned and componentDidUpdate will be invoked
- good place to do data loading when state/ props change
- componentWillUnmount
- optional
- is called automatically as soon a component will be vanished
- cleanup of a component
- good place to do cleanup (especially for non-React stuff)
- Further livecycle methods exist but are very rarely used
- shouldComponentUpdate
- getDerivedStateFromProps
- getSnapshotBeforeUpdate
- constructor
The constructor part of a React Component
class App extends React.Component {
constructor(props) {
super(props);
this.state = {lat: null, errorMessage: ''};
}
can be substituded easily with an instance property
class App extends React.Component {
state = {lat: null, errorMessage: ''};
Babeljs will take care about generating the right code for the constructor and super(props).
- passing a state property as prop to a sub component
- when state property changes then the component is rerendered and also the sub component is rerendered automatically
- import css file in component using
import 'Component.css';
- webpack takes this information and includes the css into the index.html file
- using
ComponentName.defaultProps
object to declare default props in case the component properties were not set when instanciating
- don't use conditionals in render method
- use a helper method instead (e.g. renderContent())
- this makes it easier to maintain the the JSX layout/ structure that's returned
- refactored a function-based to a class-based component
- created a Clock component that shows the time in seconds with componentDidMount, interval and state change with rerender
- created a new react app called pics
- creating a multi component structure with components folder in src folder
- new SearchBar component for my project
- added sematic UI CSS to my project and did some styling with it
- added an event Handler to SearchBar component with onChange event
- onChange: user changes text in an input
- onClick: user clicks on something
- onSubmit: user submits a form
- community convention for event handling methods
- on (prefix)
- Input (tagname)
- Change (eventname)
- debugged metasfresh WebUI and found out why the Webcam usage was failing in newer Browser versions
- Reason was a deprecated method that was used in the Image component
- fixed and tested
- Reading into JSDoc, starting to do the setup for metasfresh WebUI project
- Watching a webcast about howto reach code coverage with cypress testing
- learned an alternate way of adding an event handler to an onChange event
// Approach with callback function reference
onChange={this.onInputChange}
// Alternate approach for oneline functions with arrow function
onChange={e => console.log(e.target.value)}
- controlled
- user types input
- callback gets invoked
- we call setState with the new value
- component rerenders
- input is told what value it has (coming from state)
value={this.state.term}
onChange={e => this.setState({ term: e.target.value })}
- uncontrolled
- to know the value one has to get it from input dom element
- after pressing
return
orenter
in an HTML for, we don't want the page to be refreshed automatically. We ofren want to prevent the browser from doing to. This can be done with overwriting the onSubmit event.
...
onFormSubmit(event) {
event.preventDefault();
}
...
render() {
return (
<div className="ui segment">
<form onSubmit={this.onFormSubmit} className="ui form">
- had a weird error today
TypeError: Cannot read property 'term' of undefined
- happened with the following code
onFormSubmit(event) {
event.preventDefault();
console.log(this.state.term);
}
render() {
return (
<div className="ui segment">
<form onSubmit={this.onFormSubmit} className="ui form">
...
- roughly: this is initially bound to the object left of the dot to the method/ function call
- some solutions to solve the case
constructor() {
super();
this.onFormSubmit = this.onFormSubmit.bind(this);
}
- alternatively can be solved in using ES6 arrwow function instead.
onFormSubmit = (event) => {
event.preventDefault();
console.log(this.state.term);
};
"emmet.includeLanguages": {
"javascript": "javascriptreact"
}
- Invoking callbacks in children
- pass callback function reference in parent via prop in child component JSX
- in callback function of child then pass the state as argument to the referenced callback function via props
Parent component
class App extends React.Component {
onSearchSubmit(term) {
console.log(term);
}
render() {
return (
<div className="ui container" style={{ marginTop: '10px' }}>
<SearchBar onSubmit={this.onSearchSubmit} />
Child component
// child
onFormSubmit = (event) => {
event.preventDefault();
// prop is the reference to callback function of App.onSearchSubmit()
this.props.onSubmit(this.state.term);
};
- install axios via npm
npm install --save axios
- Sidenote: import statements from 3rd party providers shall be above the import statements of your own App
- Developer API Key from unsplash.com
- API root is
https://api.unsplash.com/
- Authentication
Authorization: Client-ID YOUR_ACCESS_KEY
- Endpoint for search photos
/search/photos
- params object added
- headers object added for Authorization
- API root is
- axios.get() is an asynch function. The result is a promise. The promise can call a callback function as soon the response has arrived.
// promise ... then
axios.get(...).then(() => {...});
- alternative with async ... await (preferred)
async onSearchSubmit(term) {
// async ... await (preferred)
const response = await axios.get(...);
- ran into the 'this' binding issue again. this time with an async funtion.
- adjusted the function to an ES6 arrow funtion solved the issue
onSearchSubmit = async term => {
// promise
const response = await axios.get('https://api.unsplash.com/search/photos', {...}
...
});
this.setState({ images: response.data.results });
};
- created an own js file and folder structure to avoid having the API key and baseURL in Application component itself
- added the folder to .gitignore to avoid commiting to public repository
- Pics project: Using array map to iterate over API response
- created a new ImageList component
- used array map to iterate over the API response and give out the images as image tag
- currently there is a warning in console 'Each child in ... an array or iterator should have a unique "key" prop. Will take care about that next.
- to allow React to find the needed list elements to rerender in a more efficient way, it is needed to add a key property to the list component root element that is returned
return <img key={image.id} src={image.urls.regular} alt="images" />;
- adding a key prop also eliminates the warning
'Each child in ... an array or iterator should have a unique "key" prop
- adding an image alt property for a11y, filled with image.description from unsplash API response
- adding some styling to the pics search result
- included a css for ImageList component
- adding grid style for image results
- using grid-auto-rows to define the height of rows in grid styling
- new ImageCard component. This shall allow to create individual styles per Image for grid style
- let the ImageCard render itself and its image reach into the DOM and figure out the height of the image (via ReactRefs)
- set the image height on state to get the component to rerender
- when rerendering, assign a 'grid-row-end' to make sure the image takes up the appropriate space
- only add to state when it's expected that the content will change over time, e.g. it does not make sense to add the property image-height to state for a given image.
- gives access to an single DOM element. in vanilla JS the equivalent is
document.querySeletor()
- we create refs in the constructor, assign them to instance variables, then pass to a particular JSX element as props
constructor(props) {
super(props);
this.ImageRef = React.createRef();
}
- when trying to access the image height in componentDidMount() via React.createRef().current.clientHeight it shows 0. What's happening?
- the component is loaded, but the browser has not downloaded the image yet
- when checking the console.log of that image ref element it shows the clientHeight although. This happens because the browser is fancy and shows the infomraiton as one opens the object tree.
- added an event listener in componentDidLoad() that is triggered after the image is loaded.
componentDidMount() {
this.ImageRef.current.addEventListener('load', this.setSpans);
}
- when having identical key value pair in objects then it can be shortened since ES2015
// until ES2015
this.setState({ spans: spans });
// since ES2015
this.setState({ spans });
- This project shall allow to search videos using the free youtube API. The app shall have a search bar, a video player and a list of videos that match the search criteria. The search results can be selected and then the video is started. The app shall be built with ReactJS. The following component hierarchy is planned:
- App
- Searchbar
- Video Detail
- Video List
- Video Item
- App
- Created a new class based SearchBar component
- imported React (for JSX)
- added render method, return JSX
- added export statement
- Import SearchBar in App component
- added SearchBar component to JSX render method
- included semantic ui CSS to index.html
- Styling in SearchBar component
- using
className="ui segment
, this draws a rectangle - using
ui form
,field
- using
- Syling in App component
- using
ui container
, adds a margin left/ right of the SearchBar component
- using
- Styling in SearchBar component
- turning SearchBar from an uncontrolled to controlled component
- storing the search term content in state instead of html form input element
- overriding the default browser behavior of form on enter
event.preventDefault();
- Integrated the Youtube API
- Using Axios to send requests and receive responses
- created a new API Key at https://console.developers.google.com
- created a new folder src/apis and added a new file called
youtube.js
there. This file keeps the API Key as variable.
const KEY = '<api-key>';
- added the src/apis folder to .gitignore
- visited https://developers.google.com/youtube/v3/docs/search/list to find out how to use the youtube data api
- Request:
GET https://www.googleapis.com/youtube/v3/search
- Parameters:
- part: 'snippet' (will show name, description of viedo)
- q: 'searchquery'
- maxResults: 5
- Request:
export default axios.create({
baseURL: 'https://www.googleapis.com/youtube/v3',
params: {
part: 'snippet',
maxResults: 5,
key: KEY,
},
});
- new functional component VideoList
- adding new component VideoList to App render method
- passing App state as props to VideoList
- in VideoList destructuring props directly to ({videos})
- new functional component VideoItem
- creating mapp in VideoList and returning a VideoItem for each entry
- adding video title from youtube API response
- adding video thumbnails from youtube API response
- adding some styling to VideoList and Video item with SemanticUI
For the video player I want to select a VideoItem element. This shall show the seleted Video in the new VideoDetail component. How shall this be done?
- App:
- state
selectedVideo: video
- props
videos: [videos, ...]
- props
onVideoSelect: () => ()
- state
- VideoList
- props
video: video
- props
onVideoSelect: () => ()
- props
- VideoItem
Create a new method in App component and pass a reference to that method down to VideoList. Further pass the reference down to VideoItem. Any time the user clicks a VideoItem we tell VideoItem to call the callback function onVideoSelect
. When it calls the callback it passes in the video that was selected. This way it invokes a method in the App component with the video that was clicked on.
When communicating from parent to child, this is done via the props system. When communicating from child to parent this is done via callback functions.
- retrieved the video description via youtube API and added some SemanticUI style to the VideoDetails component
- surrounding div tag with
ui segment
- div tag header with
ui header
- paragraph that takes the
{video.snippet.description}
- surrounding div tag with
- added an iframe tag in VideoDetails component
- surrounded by div tag with
ui embed
style (SemanticUI will take care about the styling) - how to embed a youtube video?
- Check out youtube. Press share button on any video. Press embed. It shows an iframe example. You can see how the url is built with video identifier and video of in it. This is what is needed for the embedded video url that shall be built.
- getting rid of some warnings
- adding a title to VideoDetail iframe
- adding an alt prop to the img tage in VideoItem
- adding a key prop to VideoList div tag
- adding SemanticUI grid style for VideoDetail (11 columns) and VideoList (5 columns) component
- defaulting the video selection in VideoDetail
- in onTermSubmit() so that the first video result is shown in VideoDetail
- componentDidMount() calls the onTermSubmit with default search term
- What is Redux?
- State management library
- Makes creating complex applications easier
- Not required to create a React app!
- Not explicitly designed to work with React
- Action Creator (analogy to insurance company: customer dropping off the form)
- a function that will return a plain javascript object
- Action (analogy to the form that is transmitted to the insurance company)
- the plain javascript object that was created by the Action creator
- has a type property (describes some change that we want to make in our data)
- has a payload property (describes some context about the change that we want to make)
- purpose to describe some change to the data of our apllication
- dispatch (analogy to the form receiver at the insurance company that copies the form and forwards it to each department in the insurance company, together with selected relevant data)
- the dispatch function takes in an action and makes a copy of the action object and pass it off to a bunch of different places inside of our application
- Reducers (analogy to each department in the insurance company)
- a reducer is a function that takes in a action and some amount of existing data
- it is going to process that action and make some change to that data and return it to be centralized in some other location
- State (analogy to the compiled department data)
- a central repository of all information that has been created by our reducers
- with this our application can easily reach into the central repository to achieve needed infomration and does not have to ask every 'Department' to get the information
Example for action creators and actions
- by convention the action type is written in upeprcase with underscore
// People dropping off a form (Action creator)
const createPolicy = (name, amount) => {
return {
// Action (a form in our analogy)
type: 'CREATE_POLICY',
payload: {
name: name,
amount: amount,
},
};
};
const deletePolicy = (name) => {
return {
type: 'DELETE_POLICY',
payload: {
name: name,
},
};
};
const createClaim = (name, amountOfMoneyToCollect) => {
return {
type: 'CREATE_CLAIM',
payload: {
name: name,
amountOfMoneyToCollect: amountOfMoneyToCollect,
},
};
};
Example of dispatch
- is part of the Redux Library itself
- each dispatch runs a whole Redux cycle (see Resumee of Redux Cycle)
// wire up all our Reducer functions is done via combineReducers
const { createStore, combineReducers } = Redux;
const ourDepartments = combineReducers({
accounting: accounting,
claimsHistory: claimsHistory,
policies: policies,
});
const store = createStore(ourDepartments);
store.dispatch(createPolicy('Alex', 30));
store.dispatch(createPolicy('Jim', 40));
store.dispatch(createPolicy('Bob', 50));
store.dispatch(createClaim('Alex', 120));
store.dispatch(deletePolicy('Bob'));
console.log(store.getState());
Example of reducer
- always return a new dataset. never ever modify the old dataset and return that.
// claimsHistory Department
// oldListOfClaims default to empty array
const claimsHistory = (oldListOfClaims = [], action) => {
if (action.type === 'CREATE_CLAIM') {
// we care about this action (form!)
return [...oldListOfClaims, action.payload];
}
// we don't care about this action (form!)
return oldListOfClaims;
};
// accounting Department
// bagOfMoney default to 100
const accounting = (bagOfMoney = 100, action) => {
if (action.type === 'CREATE_CLAIM') {
return bagOfMoney - action.payload.amountOfMoneyToCollect;
} else if (action.type === 'CREATE_POLICY') {
return bagOfMoney + action.payload.amount;
}
return bagOfMoney;
};
// policies Department
const policies = (listOfPolicies = [], action) => {
if (action.type === 'CREATE_POLICY') {
return [...listOfPolicies, action.payload.name];
} else if (action.type === 'DELETE_POLICY') {
return listOfPolicies.filter((name) => name !== action.payload.name);
}
return listOfPolicies;
};
- to change the state of our app we call an Action Creator
- the Action Creator produces an Action Object. The Action Object describes exactly how we want to change data in our Application,
- the Action gets fed to dispatch function which in terms creates copies of the Action Object
- the dispatch forwards the Action Object copies to each of the different Reducers
- the Reducers process those Actions, modify and return the new data. The returned data gets formed into a new State
- the State waits until we need to update again
The only way the state object can be updated is via the dispatch function in combination with Action creators and actions. There is now other way to reach into and update the state object under Redux.
- created a new project via
create-react-app songs
- deleted everything from the src folder
- created a new public github repository https://github.com/metas-mk/songs
- joined my local git repo with the new github repo via
git remote add origin <remote repository URL>
- created a new functional App component. Class based components are mostly only needed if state is needed. With the use of Redux the need of class based components will be reduced.
- added SemanticUI to the project as stylesheet
- the songs project will have 2 components
- SongList
- shall display the list of song objects
- SongDetail
- shall display the title and the length of the given songs
- SongList
- React: Same lib I have been using all the time
- React-Redux: A bunch of helper functions to get React and Redux work nicely together.
- Redux: Redux lib
The environment can be stup in the project folder via npm install --save redux react-redux
.
Without Redux
- App stores
- List of songs
- Selected song
- App passes to Son List
- List of Songs
- onSongSelect callback
- App passes to SongDetail
- selected song
With Redux
- App, SongList, SongDetail components passing very little information inbetween each other
- Reducers (only pieces of state in this App)
- Song list reducer
- Selected song reducer
- Action creators (only creator of state change)
- select song
- 2 new components, both created by React-Redux, we use instances of them and pass props to them
- Provider (has the store/ state as props) and is redered first (even before App). The Provider has the reference to store.
- Connect. The components that need access to the store will be wrapped into an instance of the Connect component. The connect component communicates with the Provider through the context system.
- React-Redux project structure
- /src
- /actions (contains files related to action creators)
- /components (files related to components)
- /reducers (files related to reducers)
- index.js (sets up both the react and redux sides of the app)
- /src
- New folder src/actions and new file index.js (webpack will identify index.ja as default when importing the folder)
- New folder src/reducer and new file index.js with the reducers SongReducer and SelectedSongReducer. Further added the combineReducers() function in there
Wire up the Provider
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import App from './components/App';
import reducers from './reducers';
ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.querySelector('#root')
);
Now all Components in our App can make use of the redux store with help of the Connect function.
In Component the connect function is called.
export default connect()(SongList);
The connect function returns a function. This function is then invoked with the property of SongList component.
In component a new function mapStateToProps
is implemented. This function is the configuration for the connect function/ component. The function is added to connect as prop.
// state is the complete state of our Redux store
// mapStateToProps is naming convention
const mapStateToProps = (state) => {
console.log(state);
return state;
};
export default connect(mapStateToProps)(SongList);
- Import connect from react-redux
import { connect } from 'react-redux';
- Call connect and pass in the component as second function call prop, here SOngList
export default connect(mapStateToProps)(SongList);
- Define
mapStateToProps
function which always has a first argument ofstate
(Redux store) and always returns an object which is going to show up as props inside the component. This object has also a reference to the dispatch method.
// state is the complete state of our Redux store
// mapStateToProps is naming convention
const mapStateToProps = (state) => {
return { songs: state.songs };
};
- Calling Action creators from components. The Action creator is added to the connect function as second argument. The connect function adds the ActionCreator to the props of the component. This is the only way Redux knows that the function is an Action creator that can be forwarded to dispatch.
import { selectSong } from '../actions';
...
export default connect(
mapStateToProps,
{ selectSong }
)(SongList);
- Debugged my song app. The selected song was not triggered after creating and wiring up the Action creator and reducer.
- Found the Bug. Was a trivial typo in the
selectedSongReducer()
- created the new functional based component
SongDetail()
import React from 'react';
import { connect } from 'react-redux';
const SongDetail = ({ song }) => {
if (!song) {
return <div>Select a song</div>;
}
return (
<div>
<h3>Details for:</h3>
<p>
Title: {song.title}
<br />
Duration: {song.duration}
</p>
</div>
);
};
const mapStateToProps = (state) => {
return { song: state.selectedSong };
};
export default connect(mapStateToProps)(SongDetail);
- imported the new component in
App.js
and created a JSX Tag for it to be rendered
- started a new ReactJS & Redux App
blog
- it will receive data from https://jsonplaceholder.typicode.com/ and show a list of blog posts with author information
- installed a few additional libraries. new this time is
redux-thunk
- redux: the Redux library
- react-redux: integration layer between React and Redux
- axios: helps to make network requests
- redux-thunk: middleware to make requests in a redux application
npm install --save redux react-redux axios redux-thunk
If you quickly want to start a React-Redux project and don't want to start working on reducers yet, than you can just create a dummy reducer spo that your are not stalled by redux errors.
import { combineReducers } from 'redux';
export default combineReducers({
replaceMe: () => '666',
});
- Components are generalle responsible for fetching data they need by calling an action creator
- Component gets rendered onto the screen
- Component's
componentDidMount
lifecycle method gets called- for such cases a classbased component is needed to have access to the lifecycle methods
- We call an action creator from
componentDidMount
- Action creators are responsible for making API requests (this is where Redux-Thunk comes into play)
- Action creator runs code to make an API request
- the Action creator initiates the API request, the request can be done via another class or function
- will use axios to do that
- API responds with data
- Action creator returns an
action
with the fetched data on thepayload
property
- Action creator runs code to make an API request
- We get fetched data into a component by generating new state in our redux store, then getting that into our component through mapStateToProps
- Some reducer sees the action, returns the data of the
payload
- Because we generated some new state object, redux/ react-redux cause our React app to be rerendered
- Some reducer sees the action, returns the data of the
- App.js main component (functional based)
- First step fake reducer
src/reducers/index.js
import { combineReducers } from 'redux';
export default combineReducers({
replaceMe: () => '666',
});
- Action creator
src/actions/index.js
export const fetchPosts = () => {
return {
type: 'FETCH_POSTS',
};
};
- PostList.js component (class based)
- import
connect from
react-redux` - componentDidMount() Action creator call fetchPosts()
- import
componentDidMount() {
this.props.fetchPosts();
}
...
export default connect(
null,
{ fetchPosts }
)(PostList);
- new jsonPlaceholder
import axios from 'axios';
export default axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
});
- Modified Action creator index.js for fetchPosts()
- This is Bad code and not allowed in Redux!!! Action creators must return plain objects.
- Leads to error
Error: Actions must be plain objects. Use custom middleware for async actions.
import jsonPlaceholder from '../apis/jsonPlaceholder';
export const fetchPosts = async () => {
// Bad!!! cannot be done like this.
// Action creators must return plain objects
const response = await jsonPlaceholder.get('/posts');
return {
type: 'FETCH_POSTS',
payload: response,
};
};
- What's wrong with 'fetchPosts'?
- Action creators must return plain JS objects with a type property - fetchPosts is not.
- By the time our action gets to a reducer, we won't have fetched our data!
- But the code in Action cretor looks like a plain object! No. Trying the Action creator in https://babeljs.io shows to what the code is transpiledm and thats not a plain JS object. All because of the async/ await.
- What happens behind the scenes?
- componentDidMount() calls this.props.fetchPosts().
- Then Redux dispatches the Action creator and invokes fetchPosts().
- The transpiles Action creator, because of async/ await returns via case 0 the request.
- That's why we don't receive a plain object but the request
jsonPlaceholder.get('/posts')
instead - only in a further case 2 the expected plain JS Object is returned
- without async/ await the transpiled code works, but we don't receive data directly, we receive a promise to receive data some time in the future. It can happen that we don't have the data already at the time the reducer is run!
- Action creator called
- Action returned
- Action dispatched
- Reducer run
- All above happens in fractions of a ms. The data return from an async call takes much longer.
- Syncronous action creator: instantly returns an action object with data ready to be directly processed by reducers
- Asyncronous action creator: takes some amount of time for it to get its data ready to be consumed. Happens for example with network requests. To deal with thode in Redux we always have to use a middleware (e.g. Redux Thunk)
- middleware is located between dispatch and reducers
- dispatch forwards the actions to the middleware
- middleware does whatever it shall do with the actions and sends it to the Reducers to be consumed
- middleware is a plain JS function that gets called with every action we dispatch
- has the ability to stop or modify actions
- middleware is most popular for dealing with async actions (e.g. Redux Thunk) but also others exist (e.g. middleware to console log dispatches)
- Normal Redux Rules
- Action creators must return actions which are plain JS objects
- Actions must have a type property
- Actions optionally have a payload
- Rules with Redux Thunk
- Action crators can return action objects or return functions
- when returning an action object it must have a type
- when returning an action object it can have a payload
Redux-Thunk source code can be found at https://github.com/reduxjs/redux-thunk
- Action creator is called and creates an object or a function
Action
is passed into the dispatch function- dispatch sends the
Action
to the middleware Redux Thunk- inside of Redux Think middleware it decides if plain JS object is received or a function
- plain JS objects are passed along to all Reducers directly
- functions are invoked by Redux Thunk and dispatch and getState functions are added as arguments.
- the function is invoked with dispatch and waited until the request is finished
- after request is complete the dispatch action happens manually
- a new action object is created and dispatched via the dispatch function
- inside of Redux Think middleware it decides if plain JS object is received or a function
In root index.js just add a few imports and arguments to createStore()
...
import { createStore, applyMiddleware } from 'redux';
...
import thunk from 'redux-thunk';
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
);
- a reducer must return any value besides
undefined
. returningundefined
leads to an error message. - are pure
- produces
state
, or data to be used inside of the app by using the initial data received from state and the action object that has been dispatched- when starting the Redux application each reducer is automatically run one time to produce the default state value
- it receives two values.
- undefined (is then initialized by the reducer default argument value)
- some action object
- each other time the reducer runs it receives the current state as first argument and a changed action object.
- must not reach outside itself in order to decide what value to return. it's only using the previos state and action object to produce the return value.
- produces
- shall not manipulate its props/ argument
state
- is a convention. Redux will not throw any errors if one does.
- but Redux will not let React know that a change was done and therfor the components will not be rerendered
Check out combineReducers Code from the Redux project https://github.com/reduxjs/redux/blob/master/src/combineReducers.js
...
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
}
...
Lodash is a nice library that provides a lot of functions that help in JavaScript Development. It can be found here: https://lodash.com/docs/4.17.15 Just open the console on documentation page and you can try it out directly.
The user data can be fetched via /users
endpoint. Routes are provided to only fetch user data for a given id.
What shall be done in the app?
- Fetch posts
- Show posts in PostList
- Each element in PostList shows the UserHeader
- User Header is given ID of user to show
- Each UserHeader attempts to fetch its user
- Show users in each UserHeader
- Reading about how to get started with JSDoc
- Evalutating if it can be used for the open source ReactJS project metasfresh
- Opened a development branch in the repository and installed JSDoc locally
npm install --save-dev jsdoc
- Created the first docs with
.\node_modules\.bin\jsdoc .\src\ -r
- The result can be found in the default folder
.\out
- Adding a dependancy to
package.json
...
"scripts": {
"docs": "./node_modules/.bin/jsdoc "
}
- Adds a configuration file for JSDoc and adding a parm to the dependancy to use the config file. The document creation can now be done via
npm run docs
.
package.json
"scripts": {
...
"docs": "./node_modules/.bin/jsdoc -c jsdoc.json",
...
}
jsdoc.json
{
"source": {
"include": ["./src"],
"includePattern": ".js$",
"excludePattern": "(node_modules/|docs)"
},
"plugins": ["plugins/markdown"],
"opts": {
"encoding": "utf8",
"readme": "./readme.md",
"destination": "docs/",
"recurse": true,
"verbose": true
}
}
- Added an example Tutorial section to the generated JSDoc
- New folder
Tutorials
with an example fileexample-tutorial.md
. Added an option to thejsdoc.json
- New folder
"opts": {
...
"tutorials": "./tutorials"
- Per default the tutorial title is shown with the filename. To change that you can create a
totorial.json
file in the tutorials folder and do a filename title assignment.
{
"example-tutorial": {
"title": "Example Tutorial"
}
}
- Installed the JSDoc theme
better-docs
locally and saved dependancynpm install --save-dev better-docs
- After install added the template option to jsdoc.json
"opts": {
...,
"template": "./node_modules/better-docs/"
}
- Added category support to JSDocs. Now the
@category
annotation can be used to add the comment into a category.
{
...,
"plugins": ["plugins/markdown", "node_modules/better-docs/category"],
...
}
}
-
I dived into the sourcecode of metasfresh-webui-frontend and started documenting the component structure of the project.
-
Found out how to document functional React components and their props. Here an example:
/**
* Separator for element groups
* @param {object} props Component props
* @param {string} props.title Separator title
* @param {bool} props.collapsible Separator collapsible
* @param {bool} props.sectionCollapsed Separator collapsed
* @param {*} props.idx Index
* @param {function} props.onClick Callback function for onClick Handler
* @param {*} props.tabId Tab ID
*/
- Further examining I the ReactJS code of metasfresh-webui-frontend and documenting the component structure of the project. Event though I'm not understanding everything in detail directly, documenting is a great way to learn step-by-step how the application works and helps other new peopls to the project to have a better prepared ressource to learn.
-
Working on my blog coding challenge project. Created a new action creator
fetchUser()
. This one fetches a user object for a given user id prop from jsonPlaceholder. -
Created a new reducer called usersReducer. Wired the new reducer up in
reducers/index.js
combineReducers
. -
In UserHeader component created the mapStateToProps()
-
Added the UserHeader into PostList component.
-
Refactored mapStateToProps. Extracted logic from component to mapStateToProps method. The method takes another argument
ownProps
which is a reference to the component props. -
Any computation that shall be done on the Redux state, shall be done in the mapStateToProps method instead of component itself.
After finishing the practice blog project it happens that each Instance of UserHeader makes a request to the API to receive the user data. This is not efficient. If a user is already know, the user data shall be reused and the additional requests shall be avoided.
- trying lodash to solve this, using the
memoize()
function. Trying this example in the browser console at https://lodash.com/docs/4.17.15#memoize
// example
function getUser(id) {
fetch(id);
return 'Made a request!';
}
getUser(5);
The console shows a log for each request made anf in the network tab (XHR) each time a request is shown. Now adding the lodash function memoize()
const memoizedGetUser = _.memoize(getUser);
memoizeGetUser(5);
Once the memoizeGetUser()
is run with a given ID it does not create a request for the same ID again. The original function is not invoked anymore. It returns what was received previously with the identical set of arguments.
-
Installed lodash with
npm install --save lodash
. -
refactored the action creator for fetchUser
import _ from 'lodash';
...
export const fetchUser = id => dispatch => _fetchUser(id, dispatch);
// private function for memoizing action creator
const _fetchUser = _.memoize(async (id, dispatch) => {
const response = await jsonPlaceholder.get(`/users/${id}`);
dispatch({ type: 'FETCH_USER', payload: response.data });
});
The downside of this approach is that each User can only be fetched one time during a session. It can happen that User data changes an one will want to refetch the user data. This needs another solution.
The new apporach will be like this:
- New action creator fetchPostsAndUsers()
- Call
fetchPosts
- Get a list of posts
- Find all unique userId's from the list of posts
- Iterate over the unique userId's
- Call
fetchUser
with each userId
- Call
Refactored action creator. Still using a bit of lodash.
export const fetchPostsAndUsers = () => async (dispatch, getState) => {
await dispatch(fetchPosts());
const userIds = _.uniq(_.map(getState().posts, 'userId'));
userIds.forEach((id) => dispatch(fetchUser(id)));
};
- Reading into the metasfresh opensource erp frontend code based on ReactJS/ Redux
- Documenting Components with JSDoc
- Reading about props validation. I'm thinking about introducing prop-types to each component in metasfresh frontent project to be able to know all props and their format via PropTypes. PropTypes checking is only done in development mode. For performance reasons the check is not triggered in production environment. -The official documentation about prop-types can be found here https://github.com/facebook/prop-types/blob/master/README.md
- The PropTypes can be installed as follows
npm install prop-types --save
- PropTypes can be imported into project files as follows
import PropTypes from 'prop-types';
Here an example how to uses the PropTypes in your component:
ComponentName.propTypes = {
// basic types
anyProp: PropTypes.any,
booleanProp: PropTypes.boolean,
numberProp: PropTypes.number,
stringProp: PropTypes.string,
functionProp: PropTypes.function,
symbolProp: PropTypes.symbol,
arrayProp: PropTypes.array,
objectProp: PropTypes.object,
// renderable types
nodeProp: PropTypes.node,
elementProp: PropTypes.element,
// instance types
personProp: PropTypes.instanceOf(Person)
// multiple types
enumProp: PropTypes.oneOf([true, false, 0, 'Unnown'])
};
- Proceeding with the developer documentation of the metasfresh-webui-frontend project.
- Creating PropTypes for component Modal.js
- imported Redux Thunk
- wired up Thunk to Redux store via applyMiddleware(thunk) which is a function from the Redux library itself
- passed the result of that as 2nd argument to createStore()
- very time we apply an action it is sent to Thunk via Middleware
- after that the action will be sent to all of our action reducers
- the rules of our action creators changed so we can now not only return an object but also a function (outer function that returns an inner function)
- action creator that calls other action creators and then manually dispatches to reducer
- reducers with common switch syntax
- reducers always have to return a new array/ object so that Redux recognizes that we made a change to data and our component can be rerendered
- Open Boardcaster Software (OBS) streaming on PC
- Stream the desktop via Real time messaging protokoll (RTMP) server
- RTMP server broadcasts the feed to multiple viewers (viewer browser)
- web server that has a list of currently streamed videos
- selecting a video makes a request to RTMP server to receive the stream
- Proceeding with the developer documentation of the metasfresh-webui-frontend project.
- Creating PropTypes for different components
- Different pages/ screen. URL determines which content will be shown. Different components shown per page
- Login/ Logout auth system via google login
- Main Landing page with list of all different streams available, each shown with stream title and stream description underneath
- If user clicks a stream the user is taken to the video page and the stream is started
- User is not logged in
- User can view a list of all streams/ channels
- User can view video for a single stream
- User is logged in
- User can create a new stream/ channel
- User can edit a stream/ channel they have created
- User can delete a stream/ channel they have created
- Once logged in, the main page is shown.
- The title and description of streams can be edited.
- Also deleting is possible.
- Further new streams can be created.
- installed React Router to my project with
npm install --save react-router-dom
- react-router-dom provides the navigation for dom based/ browser based application
- react-router is the core navigation library (don't install this manually)
- react-router-native is the navigation for react-native apps
- react-router-redux is the binding between Redux and React Router (not needed)
- Import the needed components from 'react-router-dom'
import { BrowserRouter, Route } from react-router-dom;
- add the routes to the components like this
...
const PageOne = () => {
return <div>PageOne</div>;
};
const PageTwo = () => {
return <div>PageTwo</div>;
};
const App = () => {
return (
<BrowserRouter>
<div>
<Route path="/" exact component={PageOne} />
<Route path="/pagetwo" exact component={PageTwo} />
</div>
</BrowserRouter>
);
};
...
- React Router only considers patterns after domainname and ports definition in the url (e.g.
metasfresh.com/window/143
only/window/143
is checked by React router) - the pattern is defined in the path prop for Route component
- inside a React route application one can have multiple route components that match a given url and all show themselves
- this is a wanted functionality for nested components
- the keyword
exact
Route only considers exactly given path properties, so forpath="/" exact
only paths with only "/" is matched. If not using exact also paths like "/12345" will be matched. The function called is
extractedPath.contains(path);
- with
exact
set, the rule is changed to
extractedPath === path;
- How does a prop get interpreted which only has a keyword in JSX?
<Route path="/" exact component={PageOne} />
is identical to
<Route path="/" exact={true} component={PageOne} />
- Don't use anchor tags to navigate in React components. The anchor tags forces the browser to reload the whole application. Use Link component instead.
<Link to="/">Navigate to PageOne</Link>
-
with Link the React Router prevents the browser from navigating to the new page and fetching the new index.html file. The URL still changes. The History still sees the updated URL. It takes the URL and sends it to BrowserRouter which communicates the URL tou Route components. Route components rerender to show the new set of components.
-
This is the nature of a Single Page Application (SPA)
- Browser Router
- uses everything after the top level domain or port as the
path
- uses everything after the top level domain or port as the
- Hash Router
- uses everything after the # as the `path
- Memory Router
- does not use the URL to track the navigation
- StreamList
path: /
- StreamCreate
path: /streams/new
- StreamEdit
path: /streams/edit
- StreamDelete
path: /streams/delete
- StreamShow
path: /streams/show
- Added Header component to App component because Link component shall be a child of a Router.
- the header component is still shown on every page because it's not wired up to a path
- The App shall have an authentication. A login button
Login with Google
shall appear on the top right in the header
- store a record in a database with the user's email and password
- when the user tries to login we compare the email and password with what is stored in the database
- the user is logged in when a valid email-password combination is entered
- user authenticates with an outside provider like GitHub, LinkedIn, Google or Facebook
- user authorizes the app to access their informaiton at that provider
- the outstide provider provides different information about the user
- we are trusting the outside provider to correctly handle the identification of a user
- OAuth can be used for
- user identification in our application
- making actions on behalf of the user through our application
Infos about Google OAuth can be found here https://developers.google.com/identity/protocols/googlescopes and here https://developers.google.com/identity/protocols/OAuth2
- results in a
token
that a server can use to make requests on behalf of the user (same as for browser) - used when we have an application that needs to access user data when they are not logged in (e.g. application that checks emails also if user is not logged in)
- difficult to setup because we need to store a lot of info about the user
-
results in a
token
that a browser app can use to make requests on behalf of the user -
usually used when we have an app that only needs to access user data while they are logged in
-
very easy to set up with Google's JavaScript library to automate flow
-
my application shall have the following authentication flow
- user clicks
Login with Google
button in browser (click event handler) - code calls google JS library to start the authentication via OAuth
- the Google JS lib make an authentication request to Google platform
- Google then shows a popup confirmation screen which the user shall accept. The popup screen then closes.
- Google JS library invokes a callback function in our ReactJS/ Redux application. It is provided with an authorization token ans profile information for the given user. This is identifying the user and prove that the user is sucessfully logged in.
- Logout is handled in a similar way via callback.
- user clicks
- Open console.developers.google.com and create a new project
- wait for the project to be created and then select the project
- create new credentials for the project
- create OAuth client ID
- set authorized JS origin to http://localhost:3000 for practice project
- set up an OAuth confirmation screen and generate an OAuth client ID
- install Google's API library, initialize it with the OAuth client ID
- make sure the lib gets called any time the user clicks on the
Login with Google
button - install the google api via index.htmlheader and script tag
<script src="https://apis.google.com/js/api.js"></script>
- open the browser console of the web application and check if google api is sucessfully loaded via
gapi
. It should return an object.- gapi only has one method
load
. This is so that the api can be kept very small for distribution. To receive further functionalities one must use load to retrieve them.
- gapi only has one method
class GoogleAuth extends React.Component {
state = { isSignedIn: null };
componentDidMount() {
// wire Google API to our project
// Loaded additional code `client:auth2
window.gapi.load('client:auth2', () => {
// initialize the client
window.gapi.client
.init({
clientId:
'293501909631-886gvf9254f4ltm8idakop16ntstvpae.apps.googleusercontent.com',
scope: 'email'
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
// update component level state shall rerender the component
this.setState({ isSignedIn: this.auth.isSignedIn.get() });
});
});
}
...
- googled for
gapi documentation
and checked out Authentication - tried the API via browser console and already initialized client (see above).
const auth = gapi.auth2.getAuthInstance();
auth.signIn(); // Google auth Popup opens
auth.isSignedIn.get(); // Is the user signed in?
Digging a bit into the Object one receives after gapi.auth2.getAuthInstance().isSignedIn
. The get()
method is not shown there. This method is provided via prototype inheritence. Another interesting method in proto is listen()
. This is a mtehod one can pass a callback function to. It will be invoked every time a authentication status is changed.
- added the listen call to my GoogleAuth component and wired it up with a callback function that updates state.
...
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
// update component level state shall rerender the component
this.setState({ isSignedIn: this.auth.isSignedIn.get() });
this.auth.isSignedIn.listen(this.onAuthChange);
});
});
}
onAuthChange = () => {
this.setState({ isSignedIn: this.auth.isSignedIn.get() });
};
...
-
tested in console manually signing in/ out to see if the status changes in the app
gapi.auth2.getAuthInstance().signIn()
gapi.auth2.getAuthInstance().signOut()
-
created some helper methods to use as event handlers for onClick event in Google buttons
...
onSignIn = () => {
this.auth.signIn();
};
onSignOut = () => {
this.auth.signOut();
};
...
- technically it is not needed to create helper methods, bit it makes it clearer for everybody reading the code top-bottom to find out what is going on.
- Google Auth component handles the
onSignInClick()
andonSignOutClick()
invocations. These connect to GAPI Auth2 and trigger if the signIn status has changed via callbackonAuthChange()
onAuthChange()
shall trigger Action creatorssignIn()
andsignOut()
- the Action creators shall mutate the auth state
isSignedIn: true/ false
in Redux store - the application shall receive this information then from there
This approach is not following the Redux conventions at it's best. The Redux state shall be only updated via Action Creator and dispatches to Reducers. But this way I would have to move changeAuth()
function to an action creator and therfor would spread the functionality of GoogleAuth outside of the component. For later GoogleAuth reference purpose I'm keeping it in the GoogleAuth component.
-
added the action creators into GoogleAuth component and wired to
onAuthChange()
-
received the userId from GoogleAuth to be used in the app via
gapi.auth2.getAuthInstance().currentUser.get().getId();
- installed browser extension for redux dev tools. The download and installation is described here
https://github.com/zalmoxisus/redux-devtools-extension
- install extension from webstore
- go to
Advanced store setup
to wire it up in the app project root index.js file
...
// applyMiddleware & compose
import { createStore, applyMiddleware, compose } from 'redux';
import App from './components/App';
import reducers from './reducers';
// compose
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
);
- tried out Redux dev tools with my coding practice app
- tried out Redux dev tools with airbnb.com
- very useful to keep Redux store content over refreshes can be done with
http://localhost:3000?debug_session=<some_string>
in browser address
- installed redux-forms via
npm install redux-form@8.1.0
(was not recommended installing 8.2.0 at this time)
Handling inputs without Redux
- class based components with component level state
- took element content from state an put it into the input element
- when text was changed we had an onChange event which updated the state
Handling inputs with Redux
- input element has a value and an onChange handler
- handler triggers the Redux Form action creator and maintained by Redux Form Reducer
- reducer will hold the state of our input element (content of the form data)
- something like mapStateToProps to pass our content into input elements as values.
- Redux form will essentially do all of this automatically
How to use redux-form?
- import into
reducer/index.js
and add it to the combineReducers object
...
import { reducer as formReducer } from 'redux-form';
export default combineReducers({
auth: authReducer,
form: formReducer
});
- added a first Field component to StreamCreate
- in
<Field component={comp} />
a component (e.g. input) has to be wired. The component receives a bunch of props including callback handlers via redux forms, this can be easily checked viaconsole.log(formProps)
- whenever a property appears in Field component that is not known how to handle, Redux Form just passes it as additional property. This way it can be easily used in the render method (see label beyond)
import React from 'react';
import { Field, reduxForm } from 'redux-form';
class StreamCreate extends React.Component {
renderInput({ input, label, meta }) {
console.log(meta);
return (
<div className="field">
<label>{label}</label>
<input {...input} />
<div>{meta.error}</div>
</div>
);
}
render() {
return (
<form className="ui form">
<Field name="title" component={this.renderInput} label="Enter Title" />
<Field
name="description"
component={this.renderInput}
label="Enter Description"
/>
</form>
);
}
}
export default reduxForm({
form: 'streamCreate',
})(StreamCreate);
- Proceeding with coding of my twitch like project. Wiring up the event handling with Redux forms.
- How to
preventDefault()
with Redux Forms? No need to call that method anymore, Redux Forms takes care about that. onSubmit() is not called with an event Object in this case, instead it has the values of the Fields inputs:
...
onSubmit(formValues) {
console.log(formValues);
}
render() {
return (
<form
onSubmit={this.props.handleSubmit(this.onSubmit)}
className="ui form"
>
...
- new method
validate(formValues)
that is used by Redux Forms to do some valdation of form Fields.
const validate = (formValues) => {
const errors = {};
if (!formValues.title) {
errors.title = 'You must enter a title';
}
if (!formValues.description) {
errors.description = 'You must enter a description';
}
return errors;
};
export default reduxForm({
form: 'streamCreate',
validate,
})(StreamCreate);
- the validate function is initially rendered or as soon the user interacts with it
- the validate function gets called with all values from the form
- if the user enters valid inputs, the validate function returns an empty object
- if the user enter invalid input, the validate function returns an object with key-name pairs (name of field as key, name has the error message to be shown) for each invalid field.
- Redux Form then automatically rerenders the component with each Field with error showing an error message from the returned object.
- only showing the error messages when a field is touched. This can be done with the meta property and touched
- Streams application shall use the
json-server
package - REST-ful conventions in the stream application and how they will be used
- List all records: GET (route
/streams
) => Array of records - Get one particular record: GET (route
/streams/:id
) => Single record - Create record: POST (route
/streams
) => Single record - Update a record: PUT (route
/streams/:id
) => Single record - Delete a record: DELETE (route
/streams/:id
) => Nothing
- List all records: GET (route
The new directory has to be initialized for the use with npm. Just type npm init
in the directory. npm creates a package.json
that will be used to configure the new environment.
Installed the JSON Server with npm install --save json-server
.
Created a new file in API folder called db.json. This will be used as the database for the practice project.
{
"streams": []
}
Created a new entry for scripts, to start the JSON server
...
"scripts": {
"start": "json-server -p 3001 -w db.json"
},
...
- installed axios and redux-thunk
- new apis folder with streams.js file. This file imports axios and stores the configuration for axios baseURL.
- new action creator for
createStream()
export const createStream = (formValues) => async (dispatch) => {
streams.post('/streams', formValues);
};
- wired up Redux Thunk into root index.js
...
import reduxThunk from 'redux-thunk';
...
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
composeEnhancers(applyMiddleware(reduxThunk))
);
...
After Redux Form integration this is the status quo.
export default reduxForm({
form: 'streamCreate',
validate,
})(StreamCreate);
Solution 1:
export default connect()(
reduxForm({
form: 'streamCreate',
validate,
})(StreamCreate)
);
Solution 2 (preferred one because easier to read/ more transparent what is happening):
const formWrapped = reduxForm({
form: 'streamCreate',
validate,
})(StreamCreate);
export default connect(null, { createStream })(formWrapped);
- new type entry for
CREATE_STREAM
- imported new type to action creator index.js
- added dispatch with action object to
createStream()
method
- Because of RESTful conventions (see above) I was able to code all action creators at once.
...
export const createStream = formValues => async dispatch => {
const response = await streams.post('/streams', formValues);
dispatch({ type: CREATE_STREAM, payload: response.data });
};
export const fetchStreams = () => async dispatch => {
const response = await streams.get('/streams');
dispatch({ type: FETCH_STREAMS, payload: response.data });
};
export const fetchStream = id => async dispatch => {
const response = await streams.get(`/streams/${id}`);
dispatch({ type: FETCH_STREAM, payload: response.data });
};
export const editStream = (id, formValues) => async dispatch => {
const response = await streams.put(`/streams/${id}`, formValues);
dispatch({ type: EDIT_STREAM, payload: response.data });
};
export const deleteStream = id => async dispatch => {
await streams.delete(`/streams/${id}`);
dispatch({ type: DELETE_STREAM, payload: id });
};
...
Instead of returning an array of streams - as practiced before - this time will return an object. Each object element will be a stream. The key for each element will be the id of the stream. With the object approach the updating process will be much more straightforward
- update => {...state, name:'Hugo'}
- adding a property => {...state, age: 30} -- removing a property => {...state, age: undefined} or _.omit(state, 'age')
This will be used in reducer return additionally with key interpolation. Will be look like this for example:
const streamReducer = (state = {}, action) => {
switch (action.type) {
case EDIT_STREAM:
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
Important: Redux only interpretes a change if the object is really changed and not just adjusted. That's why a new object is created, the old state is spreaded, and new adjustments are included.
- Installed and used lodash to handle the DELETE_STREAM case in reducer. The omit() function creates a new state object without the deleted element.
_.omit(state, action.payload);
- using lodash to map an Array to a new object and assing the value of a property as object key with
_.mapKeys()
_.mapKeys(action.payload, 'id');
- implementing the StreamList component
- refactoring it to a class-based component so I can use the
componentDidMount()
method to trigger the actionfetchStreams()
- created a
mapStateToProps()
method and turned the Object of streams into an array withObject.values()
const mapStateToProps = (state) => {
return { streams: Object.values(state.streams) };
};
- new
renderList()
method that maps the stream array and creates stream entries - called
renderList()
then viarender()
method - retrieving the userId from state via
getState()
in action creator
...
export const createStream = formValues => async (dispatch, getState) => {
const { userId } = getState().auth;
const response = await streams.post('/streams', {...formValues, userId});
dispatch({ type: CREATE_STREAM, payload: response.data });
};
...
- created a new helper method to find out if the mapped stream is my own stream. In such cases I want to render an edit and delete button.
...
renderAdmin = stream => {
if (stream.userId === this.props.currentUserId) {
return <div>Edit/ Delete</div>;
}
};
...
- added a
renderCreate()
button and helper method with a link to/streams/new
Intentional Navigation: User clicks on a link to move to another page.
Programmatic Navigation: The user is forced to another page through code.
Create a BrowserHistory for programmatic Navigation. New file in src folder called history.js
. This file shall have the following code:
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
This will create a history Object that can be maintained manually instead of automatically through the react-router-dom. For that it's needed to switch from BrowserRouter
to Router
in App.js and to use history as property fot the Router component.
- imported history object in action cretor
index.js
- used history object in
createStream()
after dispatch viahistory.push('/');
which navigates to url root
URL-based selection: Put the ID of the stream being edited into the URL.
Selection Reducer: When a user clicks on a stream to edit it, use a selectionReducer
to record what stream is being edited.
- swapped the button element in
StreamList.js
against a tag with theLink
component
...
renderAdmin = stream => {
if (stream.userId === this.props.currentUserId) {
return (
<div className="right floated content">
<Link to={`/streams/edit/${stream.id}`} className="ui button primary">
Edit
</Link>
<button className="ui button negative">Delete</button>
</div>
);
}
};
...
- added a small change to
App.js
and modified the path for the StreamEdit route
...
<Route path="/streams/edit/:id" exact component={StreamEdit} />
...
- the usage of
:anything
in a route path allows to use that as variable. the colon is the magical character here - when using react-router-dom the Route component automatically adds a bunch of props that can be used for the navigation
With React-Router, each component needs to be designed to work in isolation. That means each component has to fetch its own data!
- one shall not assume that data might have already been loaded somehwere in the application.
- making sure the
StreamEdit
component fetches the stream data beforehand - refactored the component to a class besed component and created the
componentDidMount()
method to initially fetch the stream for the given id - this shall be done in other components in similar war to ensure that the needed stream data is loaded initially into Redux store
...
import { fetchStream } from '../../actions';
class StreamEdit extends React.Component {
componentDidMount() {
this.props.fetchStream(this.props.match.params.id);
}
...
export default connect(
mapStateToProps,
{ fetchStream }
)(StreamEdit);
StreamEdit
andStreamCreate
components are very similiar besides 3 things- Title
- input fields with or without iniial content
- action creator that is triggered when pressing the submit button
- abstract out the form logic to a new component
StreamForm
and pass down some initial values forStreamEdit
and a onSubmit callback function forStreamEdit
andStreamCreate
- created a new component
StreamForm
- took everything from
StreamCreate
and created a newStreamForm
component with different changes
- took everything from
- reafactored
StreamEdit
which is also wrapped withing ReduxForm- using sprecial props
initialValues
with keystitle
anddescription
to automatically add these toStreamEdit
properties with the same name. This a a convenient feature of ReduxForm to achieve this kind of stuff. - using lodash to only pick the properties needed for StreamEdit. Only updated properties shall be returned.
- using sprecial props
render() {
if (!this.props.stream) {
return <div>Loading...</div>;
}
return (
<div>
<h3>Edit a Stream</h3>
<StreamForm
initialValues={_.pick(this.props.stream, 'title', 'description')}
onSubmit={this.onSubmit}
/>
</div>
);
}
- tested the new StreamEdit and now happens that the userId is lost after update. Debugging.
- found that I'm using PUT request in action creator for
editStream()
. PUT always updates/ replaces all properties of a record. - changed to PATCH request (only updates some properties of a record) solved the issue.
- found that I'm using PUT request in action creator for
...
export const editStream = (id, formValues) => async dispatch => {
const response = await streams.patch(`/streams/${id}`, formValues);
dispatch({ type: EDIT_STREAM, payload: response.data });
history.push('/');
};
...
It seems to be surprisingly difficult to create a modal overlay with React. Everything has to be nested into the div with id 'root'. A modal component would have to be nested into the page hierarchy like this example:
body>div 'root'>Provider>App>Route>StreamDelete>Modal>Button
A solution to get around this could be React Portals.
- created a new component file
Modal.js
import React from 'react';
import ReactDOM from 'react-dom';
const Modal = (props) => {
return ReactDOM.createPortal(
<div className="ui dimmer modals visible active">
<div className="ui starndard modal visible active">
Test Modal Test Modal
</div>
</div>,
document.querySelector('#modal')
);
};
export default Modal;
- added a div with modal id in index.html
...
<body>
...
<div id="root"></div>
<div id="modal"></div>
...
</body>
- added Modal to StreamDelete component.
...
import Modal from '../Modal';
const StreamDelete = () => {
return (
<div>
StreamDelete
<Modal />
...
- added an onClick event to outer div in Modal.js. This event shall route to
/
as soon a user clicks outside of the Modal. If the user clicks on a child element of that div, the user is also navigated back. Why?- this is because of the so called event propagation. As soon an event is triggered, the event is bubble up to the parents as log there is no handler found to react on it. One way to interrupt this, is to stop the propagation via
onClick={e => e.stopPropagation()
in a child element.
- this is because of the so called event propagation. As soon an event is triggered, the event is bubble up to the parents as log there is no handler found to react on it. One way to interrupt this, is to stop the propagation via
- using
React.Fragment
in JSX to bundle elements with a surrounding div tag. If wrapped into React.Fragment this is possible. These tags can also be shortened with<> ... </>
- adjusted the route in
App.js
so that the:id
variable is available for StreamShow - added a Link component to the StreamList entry header title. Now when clicking the link the user is navigated to the StreamShow page.
- the new route in Apps.js leads to showing an additional component when navigating to StreamCreate component. Both paths match when navigating to
localhost:3000/streams/new
...
<Route path="/streams/new" exact component={StreamCreate} />
...
<Route path="/streams/:id" exact component={StreamShow} />
...
- the Switch component in
react-router-dom
can help with that. Using it tells React to only render the first route that is found and matched.
...
import { Router, Route, Switch } from 'react-router-dom';
...
<Switch>
<Route path="/" exact component={StreamList} />
<Route path="/streams/new" exact component={StreamCreate} />
<Route path="/streams/edit/:id" exact component={StreamEdit} />
<Route path="/streams/delete/:id" exact component={StreamDelete} />
<Route path="/streams/:id" exact component={StreamShow} />
</Switch>
...
- refactored initial StreamShow component to a class-based component.
- imported connect and fetchStreams action creator
- added mapStateToProps method
- exported connect
- installed a standalone RMTP Server via
npm install --save node-media-server
- documentation can be found on github https://github.com/illuspas/Node-Media-Server
- the following configration has to be done for singlecore mode in a index.js file
const NodeMediaServer = require('node-media-server');
const config = {
rtmp: {
port: 1935,
chunk_size: 60000,
gop_cache: true,
ping: 30,
ping_timeout: 60,
},
http: {
port: 8000,
allow_origin: '*',
},
};
var nms = new NodeMediaServer(config);
nms.run();
- edit the package.json and add a start script like this
...
"scripts": {
"start": "node index.js"
},
...
Props System Gets data from a parent component to a direct child component
Context System Gets data from a parent component to any nested child componennt.
- creating further developer documentation in the metasfresh-webui-frontend project https://github.com/metasfresh/metasfresh-webui-frontend
- reading about Tether (react-tether) http://tether.io/ and how it is used to position elements exactly together and avoid them splitting them on a page
I'm reading into metasfresh ERP frontend code to learn more about development with JavaScript, ReactJS and Redux.
- index.jsx
- App.js
- Login.js
- LoginForm.js
- RawList.js
- SelectionDropdown.js
Reading code at https://github.com/metasfresh/metasfresh-webui-frontend and learning how the frontend of metasfresh WebUI is build with ReactJS, Redux and more libraries. A lot of things i've learned in the last 8 weeks are used in this open source project. At the moment i'm using pen and paper to take notes and draw pictures. When ready I will include the notes into the developer documentation od metasfresh.
Debugging an Issue in metasfresh project. Timestamps were show as number value instead of string Date/ Time format in readonly Table Cells. Found the error and replaced the formatting argument for Moment.js. Pull Request created and merged. Besides that did a code Review for another issue in the frontend repository.
Debugged and fixed an issue needed for the Friday release of metasfresh. Replaced the hardcoded argument for a method though a constant object and used in several places.
Creating the initial documetation tags in metasfresh frontend project. On the way documenting the component hierachy to have a better overview how everything works together.
Also reading about not using absolute paths in imports. Using imports like ../../../Button
can make it very error prone when restructuring a project. Since CRA 3 is is possible to use absolute import paths with a jsconfig.json adding compiler options with the baseURL. After that import statements can look like this
import Button from components/Button;
Will try this in the metasfresh project too.
Further going through the components structure of @metasfresh and learning more about ReactJS, Redux and how to implement such a large project. Since the beginning of the metasfresh frontend project in 2016 I've been on the concept side most of my time. It's an amaziong feeling now to learn now how the ideas and requirements I've launched in the past have been implemented in detail and how complex everything has become in such a short time.
Documenting @metasfresh component hierarchy. It's still a long way to go completing the developer documentation since it's a large applicationm but it will help me and others a lot to learn it faster in future.
Proceeding with the developer documentation @metasfresh ReactJS frontend project. There are still a lot of components to work through and document initially. Getting a better feeling for everything the more I read and write about it. For the next days I planned to include a learning session about React context. I've seen that is used at metasfresh too and will need some backround information about hows that works and is used soon.
After a long day doing a Gap Analysis for a new customer, just did some easy documentation stuff on metasfreshj project.
Reviewed a Pull Request. Started a local webserver via npm start
in ReactJS Project and tested a Pull Request locally. Further debugged a layout problem, created an issue and prepared the solution.
Reading source code @metasfresh and creating documentation tags in components and further compületing the component hierarchy tree.
Further creating the developer documentation and component hierarchy diagram @metasfresh.
Today i debugged and fixed a layout issue metasfresh/metasfresh-webui-frontend-legacy#2385 in metasfresh frontend.
Creating the developer documentation and component hierarchy diagram @metasfresh.
Tested an issue fix about Date Format in Filter Dropdown did the code review of a Pull Request @metasfresh.
Debugging and fixing a layout issue @metasfresh.
Creating the developer documentation and component hierarchy diagram @metasfresh.
I just had to find out which files have been changed in a github repository between 28-20 days ago.
git diff --name-status "@{28 days ago}" "@{20 days ago}"
Started @metasfresh frontend in browser, started the ReactJS tools and checked which components are affected by the AD-Tab GET. Saw that it could be in Table, TableCell and TableItem among other candidates. UI checked the commits in those components and found a change in the area of object comparison which was changed in the last 28-20 days.
I'm back from a longer pause of daily coding. I am starting the 2nd round from day 1 again.
Working throught the basics of nodejs development. I intentionally decided to start with such a new topic to also repeat Javascript basics that i've learned in the last year, before my pause. AFter the first few hours I recognize that a lot of knowhow is still there and am very happy that a lot of concept is still present in my mind.
Repeating IIFE Concept that will be used a lot in NodeJS to protect code interference between modules. Using the shorthand description of an anonymous funtion encpsulates the code in the function and runs it automatically through direct invocation.
// IIFE example
// 1. anonymous function
// 2. paranthesis around it turns it into an expression
// 3. invoking
// = immediately invoked funtional expression
(function () {
var firstname = 'John';
console.log(firstname);
})();
var firstname = 'Jane';
console.log(firstname);
- require is a function, that you pass a path too
- module.exports is what the require function returns
- this works because the code is actually wrapped in a function that is given these things as function parameters
Proceeding with coding and learning of NodeJS Modules. I'm recognizing that it is much easier learning NodeJS now that I'm having the basics of JavaScript present. Went through an IIFE session yesterday very fast and did not have the impression that a lot of repetition was needed.
Learning about different module patterns.
Using require to obtain native modules from https://nodejs.org/docs/latest/api/.
var util = require('util');
Back again, willing to dedicate time and being able to set a sufficient priority to get back to my mission of learning Javascript and React properly from ground up. My priorities in the last year were totally focussed on private challenges and it did not feel right nor good for me proceeding with my mission so I paused.
Now 11 months later, challenges tackled and things moving in the right direction I have found the time and curiosity again to dive into software deleoping moduns again.
So I'm gonna start my Day 0 of "Round 2 - 100 Days of code" again. From COBOL to React.
- Declaritive vs. Imperative
- Don't touch the DOM, i'll do it
- Components like Lego blocks
- Components are plain javascript javascript functions that receive input (props) and return something that looks like HTML
- Unidirectional Dataflow
- Virtual DOM: Javascript version of the DOM (Javascript treelike Object)
- material-ui https://material-ui.com/
- react-bootstrap https://react-bootstrap.github.io/
- blueprintjs https://blueprintjs.com/
- See Elements (DOM)
- See React Component Tree/ Hierarchy (React)
- Repeating ReactJS basics
- Breaking into Components. Refactorning a tiny monster card app into ganular components to move the responsibility and behavior into the component itself.
- State vs. props. Further exercises about components.
When do we break things down to components
- Reuse components in different location
- separation of concerns
- More flexibility
- Break things down into small chunks so that component does one small thing.
- easier to test
- name of component shall reflect what it exactly does
What to think about when developing in React
- Decide on components
- Decide the stats and where it lives
- What changes when the state changes
Example from monster card app: State is created in App component and passed to the sub components as attribute/ prop. As soon as the state changes in App component it trickles down as props to all sub components that uses this state.
How to implement a search feature example?
-
Add
searchField
string tostate
-
Add a
search input
torender()
-
Add an onChange property to search input
-
the input value can be received with
e.target.value
e
synthetic eventtarget
html element of inputvalue
content of the input element
-
use
setState
for adding the value to stateSearchField
property- important! setState is an asynchronous function call. To be able to log what is set to state just use a 2nd argument function callback in setState method. This callback function will the wait until the asynchronous function has been done before logging the content.
How to destructure stae in render method?
const {monsters, searchField } = this.state;
How to filter an object prperty of state based on a search input content?
- destructure the state and receive property in const
- create a new filtered array const and use filter() + toLowerCase() to receive a filtered array
const filteredMonsters = monsters.filter((monster) => monster.name.toLowerCase().includes(searchField.toLowerCase()) );
-
Functional components:
- don't have access to state
- don't have access to constructor (class method)
- don't have access to lifecycle methods
- more or less only for rendering some html. It get's some props and returns html
Why not adding state to SearchBox?
We move up the state so that all sub components have access to it (Lifting up sate).