Skip to content

Instantly share code, notes, and snippets.

@AlexFlasch
Created August 5, 2022 15:40
Show Gist options
  • Save AlexFlasch/ad531d03a5b85b6d3fde601f129e3e4f to your computer and use it in GitHub Desktop.
Save AlexFlasch/ad531d03a5b85b6d3fde601f129e3e4f to your computer and use it in GitHub Desktop.
Learning React - Crash course (An incomplete intermediate level learning resource)

Learning React

Basics

Intermediate

Advanced

  • App state management (Redux, MobX, useReducer paired with a Context, etc)
  • Advanced prop type definitions
  • async/await, optimistic UI
  • React.lazy and Suspense for lazy/async components
  • Portals

Common third party packages

  • Styled Components
  • Jest/Enzyme/@testing-library/react
  • React Router
  • Redux, React-Redux
  • React-Select

Basics

JSX syntax can be summed up as "Javascript with HTML in it". Normally HTML isn't valid syntax in JS, but JSX syntax handles parsing it, and allows you to do lots of cool things with it!

For example, you can assign HTML to a JS variable:

const myCard = (
	<div className="card">
		<span>Hello world!</span>
	</div>
);

Of course, there are some more things to be aware of when using JSX. You might have noticed, for example, that when you want to add a class to an element, you have to use className instead of class. This is due to JS already having class as a reserved keyword, so JSX had to change it to make it compatible with traditional JS syntax. Another thing to note here is that multi-line HTML statements in JSX are grouped with parentheses. This isn't strictly required, but is generally considered best practice.

Just being able to assign HTML to variables would be pretty boring. Of course, we can also use JS within our HTML!

const myFancyClass = 'fancy-span';
const firstName = 'Alex';
const lastName = 'Flasch';

const nameSpan = <span className={myFancyClass}>Hello {firstName} {lastName}!</span>

Whenever you use a pair of brackets inside your JSX, it tells the parser that the brackets contain some JS, and the result of the expression will be put in place of the brackets. This means you can do more complex things than just insert variables!

// All valid!
<input type="text" className={isValid ? 'valid' : 'invalid'} /> // ternaries
<p>{getProductName()}</p> // functions
<span>1 + 2 = {1 + 2}</span> // even normal expressions

There are some things you can't do inside the brackets in HTML though.

// Invalid! These won't work!
<div> // traditional loops don't work (for, while, do while)
{
	for (let i = 0; i < numThings; i++) {
		arr.push(data[i]);
	}
}
</div>
<div> // if statements (and switches) don't work
{
	if (isEven) {
		return <span>I'm even!</span>
	} else {
		return <span>I'm odd!</span>
	}
}
</div>

One last thing to note for the basics of JSX is that normal DOM attributes still use kebab-case. So if you wanted a data attribute to show up in the DOM, you would still use data-my-thing={myThing}. Anything React specific uses camelCase, like the className shown above. These are called "props" in React. We'll get to those a bit later.

Components can be thought of as making your own custom HTML element. When creating a React component, your custom element will be bound by the rules of "React world" and will have to make use of React-specific methodologies. These include a component's lifecycle, local state, and JSX syntax, to name the more common pieces.

Components can be as complex, or as simple as you'd like, however, you can think of them similarly to how classes should be used in OOP languages like Java, C#, C++, etc. Generally you want your components to be simple enough to understand at a glance, and single-purpose. You generally don't want your <TodoItem /> to handle both displaying Todo text, as well as adding more <TodoItem />s.

If your component feels too niche, it likely could use some splitting out. Think of it like a LEGO brick. You wouldn't have much use for an L shaped brick, but if you split that L shaped brick into 2 bricks, one long, and one short, that make up the L, you'll find more use for both of those bricks separately than if they're tied together.

In React, you can define a component one of two ways, as a class, or as a function. We'll get into the specifics of the differences a bit later.

// class component
class TodoItem extends React.Component {
	constructor(props) {
		super(props)
	}
	
	render() {
		return (
			<div className="todo-item">
				<p>TODO:</p>
			</div>
		);
	}
}

// function component
const TodoItem = props => {
	return (
		<div className="todo-item">
			<p>TODO:</p>
		</div>
	);
};

Either of these allows you to use your component as if it were an HTML tag!

<div className="todo-wrapper">
	<TodoItem />
</div>

Note that when you're using a component in JSX, the names are case sensitive. Components are almost always named in PascalCase as best practice.

We briefly mentioned props when learning JSX syntax, but now let's get into them a bit more. What is a prop? If you consider a component to be a function, props would be the parameters and arguments. Let's use the example of our <TodoItem /> component that we made above and modify it a bit.

We'll use the functional component for brevity.

const TodoItem = props => {
	return (
		<div className="todo-item">
			<p>TODO:</p>
			<span>{props.todoText}</span>
		</div>
	);
};

This allows us to add some text to our <TodoItem /> when we want to use it.

<div className="todo-wrapper">
	<TodoItem todoText="Master React"/>
</div>

As you might expect, this will put Master React in the span just after our <p> tag. Of course props can make use of the bracket syntax as well, so we could pass the result of functions, expressions, or ternaries into our todoText prop.

Sometimes we have props that should only be a string, number, boolean, or any other number of JS types. Right now todoText will accept anything. We could pass it 2, or true, or even an object { foo: 'bar' }. Most of the time unexpected types will at least work, though they may have unintended results. In the case of the object though, React won't know how to render an object to HTML, so it will crash. So we need some way to safeguard our components' props so we don't cause crashes or unintended results. Luckily, React has something called prop types. These can be defined in a number of different ways depending on what technology you might be using. You can use the prop-types package, which is most common, or you can define a type for your props if you're using TypeScript or Flow. For now we'll use the prop-types package.

Let's add a prop type to our <TodoItem />

import PropTypes from 'prop-types';

const TodoItem = props => {
	return (
		<div className="todo-item">
			<p>TODO:</p>
			<span>{props.todoText}</span>
		</div>
	);
};

TodoItem.propTypes = {
	todoText: PropTypes.string.isRequired
};

Now if we try passing it anything other than a string, React will give us an error in the devtools console, but won't crash unless it is configured to on your dev environment. Not only does this safeguard our component from unintended results and crashes, but it can also help track down where things may be going wrong thanks to React's error reporting.

Its also worth nothing that you can supply default props to a component. Let's say we added categories to our <TodoItem />. While we may sometimes want to put it into a specific category, its ok if it defaults to a "general" category.

import PropTypes from 'prop-types';

const TodoItem = props => {
	return (
		<div className="todo-item">
			<p>TODO:</p>
			<p>{props.category}</p>
			<span>{props.todoText}</span>
		</div>
	);
};

TodoItem.propTypes = {
	category: PropTypes.string,
	todoText: PropTypes.string.isRequired
};

TodoItem.defaultProps = {
	category: 'General'
};

So we can render components, and pass them different props, but how can we do some initialization, or have a component remember something that happened to it? Lifecycle and State respectively will help us solve these problems.

Let's start with a component's lifecycle. React's lifecycle is quite simple compared to other libraries like AngularJS 1.x or Vue. Components in React have 3 major lifecycle stages that are commonly used. Mounting, Updating, and Unmounting. You can hook into the mount and unmount stages using lifecycle methods for class components, or hooks for functional components. For now, we'll stick with lifecycle methods in class components.

class LifecycleExample extends React.Component {
	constructor(props) {
		super(props);
	}

	componentDidMount() {
		console.log("I'm on the page!");
	}

	componentWillUnmount() {
		console.log("I don't feel so good Mr. Stark...");
	}
	
	render() {
		return <span>I'm just here so the component shows up.</span>
	}
}

This will log "I'm on the page!" when the component is rendered to the DOM, and "I don't feel so good Mr. Stark..." when the component is about to be removed from the page. Unmounting commonly happens when a parent component unmounted, it is being filtered out of an array of components, or the browser is navigating to a new page. Mounting most commonly happens on page load, but will also occur if its added to the page dynamically via user actions.

Updating, however, doesn't have a specific lifecycle method. This is because the render method in class components is called again whenever the component updates due to props or state changing. Speaking of state, let's get into that.

For this example, we'll also briefly introduce handling DOM events so we can have something to change state for. If you're looking for all the events React can handle, you can look here.

class StateExample extends React.Component {
	constructor(props) {
		super(props);

		this.state = {
			isChecked: false
		};

		this.handleCheck = this.handleCheck.bind(this);
	}

	handleCheck(event) {
		const isChecked = event.target.checked;

		this.setState(state => ({ ...state, isChecked }));
	}

	render() {
		return (
			<input type="checkbox" onChange={handleCheck} />
			<span>{this.state.isChecked ? "I'm checked!" : "I'm not checked."}</span>
		);
	}
}

You might have noticed this.handleCheck = this.handleCheck.bind(this);, and its a side effect of how this is handled in JS, in combination with classes. We'll discuss it further in the next section.

The above example shows how a component can remember its own state, mostly referred to as "local state". This allows a component to do much more on its own, and change how it functions, what child components it renders, or any other number of things. Your imagination is the limit.

One thing to be aware of with local state is that you want to keep it as simple as possible. Managing large, complex pieces of local state can get difficult and introduce bugs if you're not careful.

Traditionally functional and class components were also called "stateless" and "stateful" components respectively. This is because functional components don't have access to lifecycle methods, or the setState() method. Before React 16.8 this meant that a functional component couldn't control its own state. If you wanted a functional component that had state, it had to have its own state lifted out of it and moved to a parent class component. This is because lifecycle methods are inherited via the React.Component class, and functions are unable to inherit from classes.

These days, this is no longer true. In fact, in most cases, functional components are recommended as they provide more flexibility and are generally easier to read at a glance. The lack of state and lifecycle methods has been resolved with the addition of React hooks in React 16.8. There's a section dedicated to those later on.

Earlier in the JSX Syntax section, you may have noticed that loops, if/else, and switch statements don't function inside JSX brackets. This doesn't mean we can't loop over arrays or objects, or have branching statements that would traditionally be done with if/else or switch statements!

React tends to lean on a more functional paradigm than other web frameworks. If you're unfamiliar with functional programming, some of the building blocks of FP will be covered here. Three of the most common Array.prototype functions used in React are Array.prototype.map, Array.prototype.reduce, and Array.prototype.filter. These three functions can handle most, if not all cases that we'd want to traditionally use flow control or loops for.

Let's say we have a list of <TodoItem />s that we all want to include in a <TodoList /> component.

import TodoItem from './TodoItem';

const items = [
	{ category: 'General', text: 'foo' },
	{ category: 'Work', text: 'bar' },
	{category: 'Home', text: 'baz' }
];

const TodoList = props => {
	return (
		<div className="todo-list">
			{items.map(item => (
				<TodoItem
					key={`${item.category}-${item.text}`}
					category={item.category}
					todoText={item.text}
				/>
			))}
		</div>
	);
};

There are a couple new things here. Let's start with the map function. If you're unfamiliar with functional programming, the map function will run your anonymous function over every single element in an array, and then return you a new array where each element has had the anonymous function run on it. For a simple example:

const list = [1, 2, 3, 4];

const allPlusOne = list.map(num => {
	return num + 1;
});

console.log(list); // [1, 2, 3, 4];
console.log(allPlusOne); // [2, 3, 4, 5];

Using the map function, we can simply loop over a list of items, and return a component in JSX for each element in the list, which is exactly what we did in the first example of this section!

Secondly, the shorthand for a function that only returns JSX. If you define a fat arrow function (including functional components!) and use parentheses instead of brackets to wrap the result, it uses an implicit return around the JSX. You can compare the implicit return function with the explicit return here:

// explicitly return JSX
const Foo = props => {
	return (
		<span>{props.bar}</span>
	);
};

// implicitly return JSX
const Foo = props => (
	<span>{props.bar}</span>
);

// one line implicit returns are also possible!
const Foo = props => <span>{props.bar}</span>;

Implicit JSX returns are the same thing as a normal implicit return, just that multi-line JSX statements should be surrounded by parentheses. A normal implicit return could be something like const addOne = x => x + 1; where it implicitly returns the x parameter's value plus one.

Lastly, there's a key prop that we never defined on our <TodoItem /> component. So what's that doing there? The key prop in React is one of very few reserved prop names in React. Its purpose is to help React keep track of changes in lists of items. It isn't strictly required, but greatly improves performance because React can more easily find a VDOM (Virtual DOM, React's in-memory representation of what is in the actual DOM) node that has a distinct key prop.

One of the only things to take note of with the key prop, is that each key should ideally be globally unique across the entire app. This is why in the first example key is being set to a string of `${item.category}-${item.text}`. More than likely, there will never be two <TodoItem />s with the same category and text. It's not the end of the world if there are key collisions, but you should do your best to avoid them as much as possible.

Next, let's talk about Array.prototype.filter. This can help take the place of flow control like if/else or switch statements.

Let's say we want to filter our list of <TodoItem />s so that we are only displaying the items in the "General" category.

import TodoItem from './TodoItem';

const items = [
	{ category: 'General', text: 'foo' },
	{ category: 'Work', text: 'bar' },
	{category: 'Home', text: 'baz' }
];

const TodoList = props => {
	return (
		<div className="todo-list">
			{items.filter(item => item.category === 'General').map(item => (
				<TodoItem
					key={`${item.category}-${item.text}`}
					category={item.category}
					todoText={item.text}
				/>
			))}
		</div>
	);
};

This will filter out anything that doesn't meet the condition item.category === 'General'. Of course you can use more complex code to determine what should or shouldn't remain in the list. All you need to do is make sure whatever you don't want filtered out returns true inside the filter anonymous function.

Lastly, there's Array.prototype.reduce. This one is a bit more complicated, but its a good one to know for building objects, or iterating over them even if it can only be used on arrays. It can also be used for summing up data points through an entire list.

An extremely simple example would be summing up a list of numbers:

const nums = [1, 5, 7, 2, 3, 8];
const sum = nums.reduce((accumulator, val) => accumulator + val, 0);
console.log(sum); // 26

reduce takes two arguments. The first is the anonymous function to be run on each value in the array, and the second is the default value of the accumulator.

In the anonymous function, you want to return the new value of the accumulator. This will be supplied as the argument accumulator the next time the anonymous function is called. So if we were to break it down to the first couple iterations, it would look like:

0) accumulator = 0, val = 1, set accumulator to 0 + 1
1) accumulator = 1, val = 5, set accumulator to 1 + 5
2) accumulator = 6, val = 7, set accumulator to 6 + 7
...
5) accumulator = 18, val = 8, set accumulator to 18 + 8

return accumulator to sum
sum = 26

Of course, the default value of the accumulator can be anything, and of any type. This means you can build a new array, or make an object out of an array, or any number of other actions. The reduce function isn't used as often as map or filter, but can be very helpful to know for more complexly structured data, or in situations where the structure of the data doesn't quite match the structure that would make sense on the front end.

Intermediate

As you may have guessed, components can be used to contain, or extend other components. This allows for complex set ups where components can be made of simpler components to make one larger, more purpose-built component, or in order to make a variation of one of your components.

Let's create a variation of our <TodoItem /> component.

// ================================
// Todo Item
// ================================
import PropTypes from 'prop-types';

const TodoItem = props => {
	return (
		<div className="todo-item">
			<p>TODO:</p>
			<p>{props.category}</p>
			<span>{props.todoText}</span>
		</div>
	);
};

TodoItem.propTypes = {
	category: PropTypes.string,
	todoText: PropTypes.string.isRequired
};

TodoItem.defaultProps = {
	category: 'General'
};

// ================================
// Removable Todo Item
// ================================
import PropTypes from 'prop-types';

const RemovableTodoItem = props => {
	return (
		<div className="removable-wrapper">
			<TodoItem category={props.category} todoText={props.todoText} />
			<button className="remove-btn" onClick={props.handleRemove}></button>
		</div>
	);
};

RemovableTodoItem.propTypes = {
	category: PropTypes.string,
	handleRemove: PropTypes.func,
	todoText: PropTypes.string.isRequired,
};

RemovableTodoItem.defaultProps = {
	category: 'General',
	handleRemove: () => {}
};

Now we have a <TodoItem /> called <RemovableTodoItem /> that has an additional remove button. Anytime we use <RemovableTodoItem /> instead of the normal <TodoItem /> we'll get that extra button next to it.

<RemovableTodoItem /> also makes use of an extremely common pattern in React, where the parent component (in this case <TodoList />) gets to control exactly what happens when our remove button is clicked. That way the parent that renders all our items can remove it in whatever way it needs to.

Components don't necessarily need to render anything themselves. You could instead make a component that only supplies functionality to wrapped components. This is the general idea behind higher-order components.

While these aren't created by end developers too often, they are frequently used in third party packages you may want to use with React. We can create a simple one just to show the process.

const withSecretValue = WrappedComponent => {
	const mySecretValue = 'Hello!';

	return props => {
		return <WrappedComponent {...props} secretValue={mySecretValue} />;
	};
};

const WrappedTodoItem = withSecretValue(TodoItem);

// use it in JSX
<WrappedTodoItem category="General" todoText="foo" /> // but it also has a `secretValue` prop!

Of course this example isn't terribly useful, but higher-order components can do much more, like subscribing to some sort of data source, or in the case of React-Redux, allowing a component to directly access Redux state and functionality through props supplied to any wrapped component.

Higher-order components have been mostly overshadowed by hooks, but they're still a great thing to know for any third party packages that may not have updated to use hooks, or in one of the very rare cases where a class component is required, and thus hooks won't work. Speaking of hooks...

Hooks are just what they sound like. They're functions that allow you to "hook" into React's lifecycle and state. Two of the hooks that you'll use frequently are useState and useEffect. These, along with many other hooks, are provided out of the box with React 16.8+.

Let's start with the simplest of the two: useState. We can refactor our earlier <StateExample /> component to use hooks in a functional component rather than lifecycle and state methods in a class component.

// class component with state methods
import React from 'react';

class StateExample extends React.Component {
	constructor(props) {
		super(props);

		this.state = {
			isChecked: false // default value of false
		};

		this.handleCheck = this.handleCheck.bind(this);
	}

	handleCheck(event) {
		const isChecked = event.target.checked;

		this.setState(state => ({ ...state, isChecked }));
	}

	render() {
		return (
			<input type="checkbox" onChange={handleCheck} />
			<span>{this.state.isChecked ? "I'm checked!" : "I'm not checked."}</span>
		);
	}
}

// functional component with hooks
import React, { useState } from 'react';

const StateExample = props => {
	const [isChecked, setIsChecked] = useState(false); // default value of false

	const handleCheck = event => {
		setIsChecked(event.target.checked);
	};

	return (
		<input type="checkbox" onChange={handleCheck} />
		<span>{isChecked ? "I'm checked!" : "I'm not checked."}</span>
	);
};

Much more succinct, declarative, and, in my opinion, easier to read once you understand everything going on with the useState hook.

Let's break useState down just a bit. It only takes one parameter, which is your default value for the stateful variable. In this case we want to use false because our checkbox is unchecked by default. useState will return a tuple (in JS this is no different than an array) containing your stateful variable first, and the function to change the stateful variable's value. If you're unfamiliar with destructuring arrays, the square brackets around isChecked and setIsChecked are equivalent to saying:

// no destructuring
const result = useState(false);
const isChecked = result[0];
const setIsChecked = result[1];

// with destructuring
const [isChecked, setIsChecked] = useState(false);

And there you go! That's the useState hook! Of course your default value can be any type, and the same goes for the value you pass to setIsChecked. Though, in most cases, its best to avoid using anything but primitives in useState, as that can make state updates harder to grok at a glance. More complex state can be better managed through the use of reducers in Redux, or the useReducer hook. That will be touched on later.

Next, let's talk about the second common hook: useEffect. Well, if useState tells React we want state, useEffect must tell it we want an "effect"... But what is an effect? Effect here is short for "side effect". In functional programming there is the idea of "pure functions" and "side effects". A pure function does nothing other than taking in value(s) and outputting value(s). A side effect is when a function has results other than returning a value. For example, writing to a database, or sending an API request would be a side effect. In general, if something changed outside of the function's scope, then it's a side effect.

Ok, so what does useEffect actually do? Well, to start, it can replace the lifecycle methods we learned about along side state: componentDidMount and componentWillUnmount:

// class component with lifecycle methods
import React from 'react';

class LifecycleExample extends React.Component {
	constructor(props) {
		super(props);
	}

	componentDidMount() {
		console.log("I'm on the page!");
	}

	componentWillUnmount() {
		console.log("I don't feel so good Mr. Stark...");
	}
	
	render() {
		return <span>I'm just here so the component shows up.</span>
	}
}

// functional component with useEffect hook
import React, { useEffect } from 'react';

const LifecycleExample = props => {
	useEffect(() => {
		console.log("I'm on the page!");
		
		return () => {
			console.log("I don't feel so good Mr. Stark...");
		};
	}, []);

	return <span>I'm just here so the component shows up.</span>;
};

Again, much more compact code, but this one can be confusing without understanding the useEffect hook, so let's explain a bit. useEffect takes two parameters. The first is the anonymous function to be run during the side effect. The second is an array of dependencies. In the example above, the array is empty. This means that no changes to values inside the component will cause this side effect to run again. React will always run all useEffect hooks defined in a component when it mounts, and any functions returned by the side effect will be run before the component is going to unmount.

However, if we wanted to send an API request whenever an input changed, we could depend upon the input's value. This would allow us to only send the API request when the value we're interested changes, and nothing else. And conversely, API requests won't be sent unless one of the dependencies of the side effect has changed since it was last ran. Just a quick example of this:

import React, { useState, useEffect } from 'react';

const AutoUpdateAPI = props => {
	const [inputVal, setInputVal] = useState('');

	useEffect(() => {
		fetch('https://mysite.com/api/value', { method: 'POST', body: JSON.stringify(inputVal) });
	}, [inputVal]);

	return <input type="text" onChange={event => setInputVal(event.target.value)} />;
};

There are, however a couple rules that you must follow when using hooks:

  1. Only call hooks at the top level. Don't call them inside loops, conditionals, or nested functions.
  2. Only call hooks from functional components. They can't be called from regular JS functions.

There is only one exception to the rules: you can call hooks from inside your own custom hook.

Like was described at the end of the higher-order components section, hooks have mostly taken the place of HOCs. Let's rewrite our secret value HOC as a custom hook.

import React from 'react';

const useSecretValue = () => {
	return 'Hello!';
};

Ok, so like our HOC, this does nothing special... it looks just like a normal function to me. And that's true! But this function can make use of useState, useEffect, and any other hook that React provides! Let's maybe spruce it up by making it actually do something.

import React, { useState, useEffect } from 'react';

const useSecretValue = (isEnabled, dependency) => {
  const secretValue = 'Hello!';
  const getRandomChar = () => secretValue[Math.random() * secretValue.length];

  let currentChar;
  const [shouldGenNewChar, setShouldGenNewChar] = useState(isEnabled);
  useEffect(() => {
    if (isEnabled && depe) {
      currentChar = getRandomChar();
    }
  }, [isEnabled, dependency]);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment