First, I have to brush up on some classic JavaScript fundamentals before I get started.
At the beginning
React is a library for building JavaScript components. Think of a button component that can be reused and given parameters that change the text. It is most commonly used with JSX (or TSX) which lets you write HTML-like code inside JavaScript. You can pass data between components using props and manage its state as if it were an object.
Additionally, it’s not uncommon to treat React as a framework due to the immense ecosystem enabling things like routing using React Router and database all within React. However, at that point most would prefer to use an actual framework like next.js.
JSX
- A component must return a single JSX/TSX element, not multiple. This can be done by surrounding everything with a
<div> </div>
or, more often, a fragment<> </>
. - JSX can’t use HTML’s
class
property since it is reserved in JavaScript, so it usesclassName
in its place. - JS comments don’t work in JSX, use
{/* comment */}
- In JSX, an expression is a one-liner, therefore:
if (var) {} else {}
replaced byvar ? {} : {}
if (var) {}
replaced byvar && {}
- In JSX, to loop through an array we can use
{items.map()}
like so:
{items.map((item, index) => (
<li key={index}>{item}</li>)
))}
Routing
There are 3 main ways of doing routing, I will only be covering a method that is fairly common and readable. For posterity, the other two are 1) using createBrowserRouter
in App.jsx
which is more flexible and 2) generating createBrowserRouter
from a separate constants file containing an array of route objects.
Creating Routes
This method uses 3 main components from react-router-dom
:
BrowserRouter
- wraps the entire application to enable routing.Routes
- groups all route definitionsRoute
- takes a path and a component to generate a component
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
Navigating to a Route
It’s not as simple as providing a path for an anchor tag <a>
. You must either use a Link
/NavLink
component from react-router-dom
instead of <a>
, or use useNavigate
from react-router-dom
which allows programmatic navigation.
import { Link, NavLink, useNavigation } from "react-router-dom";
<Link to='/pricing'>Pricing</Link>
// NavLink is an expanded version of Link allowing the usage of 'isActive' to apply different styling on different pages. Great for highlighting the page you're on! You can enable exact matching with 'end' so that it only works for '/about' but not '/about/team'. Read more here: https://api.reactrouter.com/v7/interfaces/react_router.NavLinkProps.html
<NavLink
to='/pricing'
className={({ isActive }) => isActive ? "active-link" : "inactive-link"}
end
>
Pricing
</NavLink>
const navigate = useNavigate();
return (<div onClick={() => navigate(`/profile/${user.id}`)}>);
Accessing Route Parameters
For a route defined with path="/user/:id"
, we can access the id
using useParams
from react-router-dom
. Just destructuring the properties in useParams
will give access to that information.
import { useParams } from 'react-router-dom';
export default const JobDetail = () => {
const { id } = useParams();
return (<p>Job ID: {id}</p>);
};
Loading with LoaderData
This is a react-router-dom
feature that can simplify loading and managing component data while also enhancing the user experience. Rather than creating a useEffect
and managing loading data, we can attach a loader function to a route which will probably do some asynchronous function.
Let’s say we have a component JobPage
where we load a job from the API given an ID. In the Route
instance for JobPage
we can add loader={jobLoader}
. Now in JobPage
(or wherever you want), we must define jobLoader
so Route
can use it. Then, in our JobPage
component, we can simply call useLoaderData()
to grab the data.
const jobLoader = async ({params}) => {
const res = await fetch(`https://localhost:8000/api/jobs/${params.id}`);
const data = await res.json();
return data;
});
const JobPage = () => {
const job = useLoaderData();
return <h1>{job.title}</h1>
}
Props
In React, any properties you pass into your component will be available under props
, a JavaScript object. props
can optionally be destructured or given default values exactly like JavaScript.
<Hero title='Test' sub='Subtitle!' />
const Hero = (props) => { return (<h1>{props.title}</h1>); }; // plain usage
const Hero = ({title, sub}) => { return (<h1>{title}</h1>); }; // destructured
const Hero = ({title="Abc", sub}) => { return (<h1>{title}</h1>); }; // with default
Typing Props: In TypeScript, you can create a type representing all of the props
Hooks and State
Functions that enable functional components like state and other React features, notably useState
and useEffect
. Why are hooks necessary? Because they do the dirty work for you. Under the hood of React components follow a complicated life cycle which can be cleanly interfaced through the use of these hooks.
There are 5 main kinds:
State Hooks
UseState
This create a variable stored with the component that be read and written to. Must define a two-array with the first being a getter variable and the second being a setter function. It can (and should) be given an initial value. For the setter function you can either pass the new state directly, or pass a function that calculates the new state from the previous state.
import { useState } from 'react';
const [age, setAge] = useState(initialVal)
Key Idea: When you need to modify the current value, calculate the new state from the previous state by passing in a function that grabs the latest state like so:
setAge((a) => a + 1)
UseReducer
Similar to useState
but allows for custom, or more complicated logic to be extracted outside of the component.
Effect Hooks
UseEffect
Key Idea: The code here runs when 1) the component is rendered (which may happen multiple times) and/or 2) any of the dependencies (the variables in
[]
) change
Allows component to have side effects, like fetching data when the component renders. Often used in conjunction with useState
to, for example, use the fetched data. useEffect
requires two arguments:
- A function that runs once the component mounts.
- A dependency array. An array of variables that, if their value changes, the function will run. Even if there are no variables, ensure there is an empty array
[]
, otherwise, the function will continuously run.
import { useEffect } from 'react';
const JobListings = () => {
const [jobs, setJobs] = useState([]);
useEffect(() => {
// will run once on component mount
const fetchJobs = async () => {
try {
const res = await fetch('https://localhost:8000/api/jobs');
const jobs = await res.json();
setJobs(data);
} catch (error) {
console.log('Error fetching data:', error)
} finally {
setLoading(false)
}
};
fetchJobs();
}, []); // empty dependency array
}
Ref Hooks
UseRef
Let you make a reference to HTML elements and directly handle them. TODO
Event Handling
Some built in events are as follows:
onClick
onChange
onSubmit
They take a function to be run on the event like onClick={handleClick}
.
Some Housekeeping
- A component is literally just a function that returns some HTML, so a file can have multiple components (but only one default component) if desired.
- Did you know you’re probably writing a SPA? If you look at the
index.html
file, that is the single page because it loads in/src/main.tsx
as a script containing our entire app.
Component Lifecycle (sort of…)
EVENT
(page load, setState, props change) triggers re-render- Component re-runs top-bottom
- Component returns JSX
- Updates DOM
- Then: useEffect cleanup (if applicable)
- Then: useEffect callback
Some Best Practices
Goal | Tool |
---|---|
Track user input | onChange (or other event handlers) + useState |
Persist last known value across unmounts | useRef.current = val |
Do something when mounted | useEffect |
Controlled vs Uncontrolled
In short, default to controlled components unless:
- You don’t need real-time access to input state
- You’re integrating with a DOM-based or third-party system
- You’re optimizing for performance in massive forms
- You want to keep things very simple