This repo is a code-along for https://www.udemy.com/course/react-redux/
Project Overview
-
Project 1 Covers Sections (1) and (2)
- Basic JSX
-
Project 2 Covers Section (3)
- Props
-
Project 3 Covers Sections (4) - (6)
- Class Based Components
- State
- Lifecycle Methods
-
Project 4 Covers Sections (7) - (10)
- Handling User Events/Input
- API Requests
- Building Lists of Records
- Using Refs for DOM access
-
Project 5 Covers Section (11)
- Challenge
/\ /\ /\ legacy course = = = = = = = = = = = = / / / new course
-
Project 6 Covers Section (6) - (9)
- Handling Forms
- APIs
- Context System
- React Hooks
-
Project 7 Covers Sections (10) - ...
Initializing And Launching a React App
- install node
- use
npx create-react-app [appname]
to initialize a new react app - start the app with
npm start
JSX Basics
-
babel is used to "translate" JSX so it can be interpreted by basically any web browser. babel provides a nice tool to check on the fly how a given JSX is translated at https://babeljs.io/repl
-
quotes vs double-quotes: in JSX-components double quotes should always be used to indicate a string, but for non-JSX-components, single quotes are used (by community convention). but from a technical point of view, it does not matter. i'll use double quotes.
-
class
vsclassName
: an htmlclass
property should be converted toclassName
when converted to JSX. this is simply to avoid collisions between the JavaScriptclass
keyword and the property inside JSX. -
for
vshtmlFor
: for the same reason, thefor
attribute (of labels) is replaced withhtmlFor
to prevent collisions -
js variables or methods can be reference from JSX with curly braces, e.g.
<button>{ getMyVar() }</button>
would show a button with the text returned bygetMyVar()
. -
default props can be specified by configuring
const Component = props => { //... }; Component.defaultProps = { test: "Hello World!" };
This is a better approach than doing something like
<div>{ props.test || "Hello World!" }</div>
Three Tenets of Components
Component Nesting
- components can be used and nested inside each other just like standard html elements can
Component Reusability
- child components can and should "live" in their own .js files and then be exported and improted by the parent component
- components can be nested inside other components by also "implicitly" passing them as props, e.g.
will add an h1 element to
<ApprovalCard> <h1>nested h1</h1> </ApprovalCard>
props.children
- inside the component, the children can be "consumed" like this, e.g.
<div> { props.children } </div>
Component Configuration
- data can be passed grom a parent to a child component using props (e.g. with
<Comment author="Tom" />
)... - ... and then be processed by the child consuming the
props
parameter that is always passed as the first parameter, e.g.const Comment = props => { return ( <span>{ props.author }</span> ); }
Class Components
-
aren't really state of the art anymore, as Function Components got a lot more powerful with the most recent versions of React, but may be used in a lot of legacy code
-
it's also much easier
-
must extend
React.Component
-
must implement a
render()
method that returns the actual JSX -
rendering a component and returning different JSX based on state is called conditional redenring
-
it is good practice to avoid conditional rendering inside the
render()
function, but rather extract it in a seperate function and return one common parent element from render, which then calls that other function inside, e.g.renderContent() { if (this.state.errorMessage) { return <div>Error: { this.state.errorMessage }</div> } else if (this.state.lat) { return <div>Your latitude is { this.state.lat }</div>; } else if (!this.state.lat && !this.state.err) { return <div>Loading data...</div>; } } render() { return <div class="parent-class">{ this.renderContent() }</div>; }
The React State System
state
must not be confused withprops
- only works with Class Components (though Function Components offer a similar system through hooks)
state
is a JavaScript object that contains data relevant to the component- updating
state
causes the component to re-render state
must be updated by using thesetState()
methodstate
must be initialized when the component is created, e.g. in theconstructor
method- the initialization of
this.state
in theconstructor
is THE ONLY TIME where it is directly assigned without usingsetState()
!!! - the
render()
method should never contain any "business logic" that does any kind of heavy lifting or, even worse, updates the componentsstate
. that will ultimately lead to constant re-rendering - as an alternative method to initialize state in the
constructor
method, it can also be initialized as a class property right away, e.g.class MyComponent extends React.Component { state = { lat: null, err: "" }; //... }
- querying data should, by convention, be triggered inside
componentDidMount
state
andprops
can easily be confused: astate
can be used an passed down to a child component as aprop
- CSS classes that describe a child and are derived from it's state or props can easily be written with template string, e.g.
<i className= { `${iconName} icon massive icon-left` } ></i>
Handling Events
-
event handlers must be registered through a component prop, such as
onChange
, e.g.<input type="text" name="search" placeholder="Search Term" value={ this.state.searchTerm } onChange={ this.onInputChange } />
-
an input element that has a value that refers to the components state is called a "controlled element"
-
"wrong" bindings of
this
in handlers can be solved by one of 3 methods:-
"re-binding"
this
in theconstructor()
method -
defining the
onEventHandler
method using an arrow function, e.g.class Component extends React.Component { onSubmitHandler = e => { e.preventDefault(); console.log(this.state); // will properly log state to console } render() { return ( <form onSubmit={ onSubmitHandler }> <!-- form content --> </form> ); } }
-
calling the
onEventHandler
in theonEvent
prop using an arrow function, e.g.class Component extends React.Component { onSubmitHandler(e) { e.preventDefault(); console.log(this.state); // will properly log state to console } render() { return ( <form onSubmit={ e => onSubmitHandler(e) }> <!-- form content --> </form> ); } }
- data can be communicated from a child component to a parent component by passing a callback method from the parent to the child, which then calls that specific callback method when due.
-
Rendering Lists
-
a list can be rendered by simply returning an array of JSX objects from a method like so:
class ImageList extends React.Component { imageList() { return this.props.imageUrls.map(e => <li><img src={ url } /></li>); } render() { return ( <div className="ui segment"> <div>Found {this.props.imageUrls.length} images</div> <ul>{ this.imageList() }</ul> </div> ); } }
-
every element of a list needs to have a unique key property. in most cases an element's id is used for that purpose
-
the DOM can be accessed by using
refs
- in the constructor, create a property on the state object and initialize it with
React.createRef()
- in the render method, hook it up to a JSX element by setting it as the
ref
-property
class ImageCard extends React.Component { constructor() { super(); this.state = { imgRef: React.createRef(), liRef: React.createRef() }; } componentDidMount() { console.log(this.state.liRef); console.log(this.state.imgRef); } render() { return ( <li ref={ this.state.liRef }> <img ref={ this.state.imgRef } src={ this.props.url } alt={ this.props.alt } /> </li> ); } }
- in the constructor, create a property on the state object and initialize it with
-
react does not allow to return "unwrapped" multiple html elements (e.g.
return <h1>H1</h1><p>content</p>
) is not valid JSX. to achieve this result without wrapping the content into a separate html element alltogether, aReact.Fragment
can be used (e.g.return <React.Fragment><h1>H1</h1><p>content</p></React.Fragment>
)
React Hooks
- hooks can be used with functional components
useState
-
the
useState
hook can be used to add the concept of state known from class based components to functional components as well -
state with
useState
can be implemented like so:import React, { useState } from "react"; const Light = () => { const [light, setLight] = useState(false); return ( <div> <h1>Light is on: {light}</h1> <p onClick={() => setLight(true)}>On</p> <p onClick={() => setLight(false)}>Off</p> </div> ); };
-
use the functional version of state updates whenever a new state depends on the old state!
useEffect
-
useEffect
: enables lifecycle methods in functional components -
useEffect
is used like so:import React, { useState, useEffect } from "react"; const Counter = () => { const [counter, setCounter] = useState(false); useEffect(() => { console.log("counter has changed!"); }, [counter]); return ( <div> <h1>Counter: {counter}</h1> <p onClick={() => setCounter(counter + 1)}>+1</p> </div> ); };
-
the second parameter can have one of three values:
[]
: run callback at initial render onlyundefined
: run callback at initial render and after every rerender[someValue]
: run callback at initial render and after every rerender ifsomeValue
has changed
-
the callback function in useEffect itself should not be async. best practice is to define an async function inside the callback itself and then call it right away:
useEffect(() => { const fetchData = async () => { // fetch data }; fetchData(); }, [term]);
-
if the callback function refers to a variable that is not in the list of dependencies, a "stale variable reference" may occur. This means that the callback function in useEffect references a variable at a location in memory that is no longer being referenced by the component itself, as a complete new "set" of variables is created upon every rerender, e.g.:
const [counter, setCounter] = useState(0); useEffect(() => { setTimeout(() => console.log(counter), 500); }, []); return <button onClick={() => setCounter(prev => prev + 1)}>+</button>;
In this example, counter will always log 0, as the callback function will always reference the counter created upon the initial render, not the memory location that holds the actual counter value (see Video #144 for further details).
The same happens when a function is passed to useEffect.
useCallback
can be used to mitigate this behaviour. -
useEffect
can return a cleanup function, that will get executed the next time BEFOREuseEffect
runs!let i = 0; const MyComponent = () => { const [term, setTerm] = useState(""); useEffect(() => { const fetchData = async () => { // fetch data }; fetchData(); i++; console.log(i); // 0, 1, 2, ... return () => console.log("CLEANUP: " + 1); // 1, 2, 3, ... }, [term]); //... }
-
the cleanup function is a great place to clear a timout function invoked in the previous
useEffect
call...
useRef
useRef
: create aref
in a functional component
ReactDOM.createPortal
ReactDOM.createPortal(/* jsx element */, /* target */)
can be used to render content into a specific target, e.g.
const Modal = ({ isOpen, children }: ModalProps) => {
if (!isOpen) return null;
const target = document.querySelector("#modal-container");
if (!target) return null;
return ReactDOM.createPortal(
<div>
<div className="absolute inset-0 bg-gray-300 opacity-80"></div>
<div className="absolute inset-40 p-10 bg-white">
<h1>Modal</h1>
<div>{children}</div>
</div>
</div>,
target
);
};
- make sure to check that
target !== null
, otherwise the app may break!
Styling
- CSS files should not be added to the HTML directly, but rather be imported on a per component level with an
import
statement, e.g.import "./SeasonDisplay.css";
. this instructs the bundler to inject/wire the CSS accordingly - SASS/SCSS can also be used by adding the dependencies with
npm install node-sass
and then rename the CSS to SCSS and import it as described above
Context
- with context, data can be shared across the entire app.
- steps:
- create a context with
const MyContext = createContext()
- wrap the entire app inside a Context-Provider with
<MyContext.Provider><App /></MyContext.Provider>
- consume context inside components with
const data = useContext(MyContext)
- create a context with
Redux
- Redux is global state management library.
- Redux Toolkit simplifies interacting with the redux store.
- react-redux bundles redux bindings for react.
First, use createSlice
to create slices for data "classes" in your application, such as Videos and Songs in out example.
Then, init the actual store with configureStore
and hand over the reducers defined when creating the slices.
Finally, an action can be dispatched store.dispatch(songsSlice.actions.addSong("lalala"))
for example. This is called an ActionCreator
and simplifies the process of dispatching an action object and makes it pretty typesafe with typescript.
Explicitely typing the action parameter of a slice's reducer function with PayloadAction<string>
, for example, automatically types the payload properly and avoids any
typing!
Initialize a new project like so:
- Create the store as described above and export it. Also export the state's type with
export type RootState = ReturnType<typeof store.getState>;
- Import the store in the main index.ts or main.tsx file, the one that initializes the react app.
- Import the Provider component from react-redux.
- Wrap the App inside the Provider and pass it the imported store as its store property.
To update the store from a component,
- Add a reducer to the slice that you want to update. The reducer can either modify the state in place as immer is used under the hood, or return a new object.
- Export the action creator created by the slice
- Import the action createor and
useDispatch
from react-redux into the component you want to use it from - Call the useDispatch hook to get access to the dispatch function
- When the user interacts with the app, call the action creator to get the action and dispatch it.
redux toolkit uses Immer under the hood. Therefore, we can mutate the state inplace OR return a new state object from a reducer.
To access state,
- Import the
useSelector
hook from react-redux - Create a selector and pass it into the hook. It is a function that takes the store's root state as an argument. Make sure to type it with the
RootType
exported from the store. - Return the slice you are interested in.
To modify multiple slices upon a single user interaction, one could
- ... either dispatch individual actions, one for each slice. This, however, isn't the "redux way" of doing it.
- ... or create a separate action width
createAction("app/reset")
, for example and then make each slice listen for that action by addingextraReducers
.
Lazy Loading
Requests should never be made from inside reducers, hence Redux Toolkit comes with two possible means of fetching data:
Async Think Functions: This method is somewhat outdated.
- Create new file for the thunk named after the purpose of the thunk, e.g.
fetchUsers.ts
- Create the thunk with
createAsyncThunk
and give it a base "action type", i.e. string identificator, such as e.g.const fetchUsers = createAsyncThunk("users/fetch", ...)
- From inside the thunk, make the request and return the data that you want to use inside the reducer. That is, create an async function that actually makes the request to the server and pass it as the second argument to
createAsyncThunk
. - In the slice, add
extraReducers
, watching for the action types dispatched by the thunk. The actions are provided through the previously exported thunk and arefetchUsers.pending
,fetchUsers.rejected
orfetchUsers.fulfilled
. The respective data is contained in theaction.payload
property. - Export the thunk from the
store/index.ts
file - When the user does something, dispatch the thunk function to run it!
Redux Toolkit Query: This is the state of the art method!
With RTK Query you create an API for each "slice".
-
Identify a group of related requests that your app needs to make.
-
Make a new file that will create the API, e.g.
src/store/api/albumsApi.ts
-
The API needs to store a ton of state related data, requests, status, errors. Add a
reducerPath
which is simply a name that redux uses to address this reducer, e.g.reducerPath: "albums"
-
The API needs to know how and where to send requests. Add a
baseQuery
, e.g.baseQuery: "http://localhost:3005
-
Add "endpoints", one for each kind of request you want to make. Requests that read data are queries, requests that modify data are mutations.
-
Export all of the automatically created hooks, e.g.
export const { useFetchAlbumsQuery, useAddAlbumMutation } = albumsApi;
-
Connect the API to the store. Reducer, middleware and listeners:
const store = configureStore({ reducer: { // ... [albumsApi.reducerPath]: albumsApi.reducer, // this helps to avoid typos and inconsistencies }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(albumsApi.middleware), }); setupListeners(store.dispatch); //... export { useFetchAlbumsQuery, useAddAlbumMutation } from "./apis/albumsApi";
-
Re-export the hooks from the
store/index.ts
file -
Use the generated hooks in a component, e.g.:
const AlbumsList = ({ user }: AlbumsListProps) => {
const { data: albumData, error: fetchAlbumsError, isLoading: isLoadingAlbums, isFetching: isFetchingAlbums } = useFetchAlbumsQuery(user.id);
const [addAlbum, addAlbumMutationResults] = useAddAlbumMutation();
//...
}
RTK query has a tag system that allows mutations to invalidate previously run fetch requests and thus refetch data. Here's an example:
const albumsApi = createApi({
//...
tagTypes: ["albums"],
endpoints: (builder) => ({
fetchAlbums: builder.query<Album[], string>({
providesTags: (result, error, userId) => [{type: "albums", id: userId}],
query: (userId) => ({
//...
}),
}),
addAlbum: builder.mutation<Album, string>({
invalidatesTags: (result, error, userId) => [{type: "albums", id: userId}],
query: (userId) => ({
//...
})
})
}),
});
This system can be extended further by adding very "granular" tags:
const photosApi = createApi({
//...
tagTypes: ["photo", "albumPhotos"],
endpoints: (builder) => ({
fetchPhotos: builder.query<Photo[], string>({
providesTags: (result, error, albumId) => [
{ type: "albumPhotos", id: albumId },
...(result?.map((photo) => ({ type: "photo" as const, id: photo.id })) ?? []),
],
//...
}),
addPhoto: builder.mutation<Photo, string>({
invalidatesTags: (result, error, albumId) => [{ type: "albumPhotos", id: albumId }],
//...
}),
deletePhoto: builder.mutation<Photo, string>({
invalidatesTags: (result, error, photoId) => [{ type: "photo", id: photoId }],
//...
}),
}),
});
Typically you would decide for one of the two options for the scope of one project. In the example project, we'll use both for demo purposes.
Other Learnings
- semantic ui is a nice css library to quickly style components without heavy customization in the first place (https://semantic-ui.com/ and https://cdnjs.com/libraries/semantic-ui)
- faker is a great library for creating example data for prototyping and testing (https://fakerjs.dev/guide/)
- the chrome developer console's "Sensors" tab provides a way to override sensor data for testing and mocking purposes
- HTML entities (like
"
etc.) will not be decoded by JSX per se. UsingdangerouslyInsertHTML
is an option, but not good practice, as it makes an App XSS vulnerable. A better option would be to run those strings to an HTML entity encoder like he, for example. - json-server is a great package to quickly spin up a development rest server that uses a simple json-file as its "database".
- use concurrently to spin up frontend and backend with one command
- the vscode extension REST client can consume http-files and send requests directly from here.
- the
classnames
npm library allows for conditionally joining classnames. nice! - use
React.ComponentPropsWithoutRef<"button">
(for example) to inherit the properties of a native component (see project 7, Button.tsx for an example); alternativelyReact.ButtonHTMLAttributes<HTMLButtonElement>
could be used. - when writing deconstructuring assignments, order matters:
const obj = {a: "a", b: "b"}
const first = {...obj, a: "modified a"}
const last = {a: "modified a", ...obj}
console.log("first", first) // GOOD: "first", { a: "modified a", b: "b" }
console.log("last", last) // BAD: "last", { a: "a", b: "b" }
Helpful Libraries
- classnames
- react-icons
- immer
- json-server