Core React Concepts

Core React Concepts

useState Hook

import { useState } from 'react'; const Component = (props) => { const [name, setName] = useState('Franklin'); // 'Franklin' here is default state for name // setName is a function you can call to change the state return ( // ... ) };
Remember: State is immutable, meaning you can't change it here, you 'send it down' one way only
That's why hooks are beautiful, to change the state of name, just run something like setName('Nilknarf');
Also! You can, and often will, pass objects here, not just strings

useEffect Hook

Runs every single time a component renders
import { useEffect } from 'react'; useEffect(() => { const fetchTasks = async () => { const res = await fetch('<http://localhost:5000/api>') const data = await res.json() } fetchTasks() }, []) // Empty array at the end if you want all items, or fill the array with keys of exactly what you want

Rerender Only When Desired

useEffect always renders AFTER the component, literally the UI renders before useEffect triggers
useEffect rerenders every time, but what if you don't want it to for unrelated actions?
Dependency array: render only what and when you want it to:
const [count, setCount] = useState(0); const [darkMode, setDarkMode] = useState(false); useEffect(() => { console.log('Runs once') }, []) // NO dependencies, runs only once on component render useEffect(() => { document.title = `${count}` }, [count]) // ONLY updates title when the count state changes useEffect(() => { localStorage.setItem("darkMode", darkMode) }, [darkMode]) // ONLY stores value when darkMode state changes

Fetch On Component Load

yarn add axios
useEffect itself cannot return anything, the only thing it can return is 'cleanup' code.
The workaround is we call a function within useEffect that itself gets the data (asynchronously), in this case the fetchData can return a promise but useEffect cannot.
Best way to store and use the return data is in state:
const [posts, setPosts] = useState([]); useEffect(() => { const fetchData = async () => { const result = await axios.get('<https://jsonplaceholder.typicode.com/posts>'); setPosts(results.data) // the data param has the data in an array for this api } fetchData() }, []) // If you don't have an empty dependency array, you'll infinite loop

Fetch On Change/Search

yarn add axios
Search the api every time a letter is added to an input field
const [search, setSearch] = useState(""); // Stores values within the search const [brewery, setBrewery] = useState([]) // Stores the api data in an array
<input value={search} onChange={(e) => setSearch(e.target.value)} type="text"/>
useEffect(() => { const fetchData = async () => { const result = await axios.get(`https://api.openbrewerydb.org/breweries/search?query=${search}`) setBrewery(result) console.log(brewery) } fetchData() }, [search])
The api is a generic one with the query search. When we add a letter to the input, the search state changes. Because the search state changes, it updates state which
  1. Updates the value of the input to show the user
  1. Triggers a rerender of the component. Note the dependency array at the end of the useEffect
Because this useEffect only updates when search changes, it reruns the fetch request and thus searches every time the user enters another letter. Right now it just console logs. If the dependency was empty it'd load once on page load, if we had no dependency array then it'd search whenever literally any other states or props changed.

Routing

yarn add react-router-dom
import {BrowserRouter as Router, Route} from 'react-router-dom';` In App.js
Within App.js, wrap everything with the aforementioned <Router> tags:
return ( <Router> <Header /> <Content /> <Footer /> </Router> )
Create a new component that you'll route too (eg an About page):
For any components you plan to link to another route from (eg App.js and an about component), import:
import { Link } from 'react-router-dom';
Now we can add the Routes:
<Route path='/about' component={About} />
The above will show the 'about' component whenever the route is changed to /about
To change the route from / to /about we need to wrap something with a <Link />, we'll just use text here:
<Link to='/about'>About</Link>
Now to go back we'll need to link to /, we'll again use text from somewhere on the about page:
<div className='about'> <Link to='/'>Go Back</Link> </div>
To setup your index page you'll need it to be wrapped in it's own Route aswell, or else it will appear on all other pages:
<Route path='/' exact render={(props) => ( // exact ensures that nothing after the '/' will show, always use exact <> <IndexComponent1 /> <IndexComponent2 /> <IndexComponent3 /> </> )} />
If you have a component showing up when you dont want it to, e.g a login button in your header but don't want it to appear on the about page, we can add a 'useLocation' hook so that it only appears when the route in the http bar matches identically. First, import the hook within the component containing the thing you wish to make disappear:
import { useLocation } from 'react-router-dom';
Then wrap the component/JSX in it:
return ( <> <h1>Header Text</h1> {location.pathname === "/" && <LoginButton />} </> )
The above will only appear when the route is exactly / and not something like /about.
 

ES6 Components

const Component = (props) => { const place = 'world'; return ( <h1>Hello {place}</h1> ) } export default Component;
No import react is necessary at the top these days
Props are imported within the arrow function
When importing a component you can pass it props like this:
<ChildComponent prop1='something' prop2='something else' prop3='final thing' />

Object Destructuring

When importing props to a component, you can destructure them within the components arrow function like so:
const Button = ({ color, text }) => { return ( <button className='btn' style={{backgrounColor: color}}> {text} </button> ) }

Default Props

These are values you want your component to have if no values are parsed in from the parent component
Place these blocks just underneath const Component =... but above the export default Component
Component.defaultProps = { color: 'blue', text: 'Submit' }

Looping with .map()

If you have an object to loop over, create an arrow function to select elements from each if it's a list like this:
const tasks = {} //Imagine this is full of JSON const Tasks = (props) => { return ( <> {tasks.map((task) => ( // task here is like 'for task in tasks:' <h3 key={task.id}>{task.keyinside}</h3> ))} </> ) }
Why add key=task.id??? Every item in a list needs a key for some reason, in the object (even though it isnt shown) each item has a unique id.
As long as it's something distinct to the item then all will be fine, it could have even just been task.text in this case
 

Prop Drilling

Drilling is the technique of passing values/objects/functions down from component to component from some sort of master component. You can also use the context API but it’s often better to drill, as long as you don’t go too far, you decide what the definition of that is
const [name, setName] = setState('Franklin');
Let's say you want to delete an item by id. Pass down a list of names aswell as a function that might look like this:
const deleteName = (id) => { // You'll pass the id back up from the component that has it console.log('deleted: ' name.id); }
For each prop to pass down it'll look like this:
(The first onDelete is what the prop will be called in the next component, the second is the prop that is was on the current component.)
<Person key={name.id} onDelete={onDelete} />
When a function is passed down is goes from something like: deleteName to onDelete as convention
  • -
The final component could have something like a button that will look like this:
<button onClick={() => onDelete(name.id)}></button>
  • -
Let's say you have a list of items and you want to delete this item, use filter() in the original function:
const deleteName = (id) => { setName(name.filter((value) => value.id !== id)) }
filter.() iterates over the given array and creates a new one.
For each item that does not have the same id as the one given to be deleted, it will allow to be added
 
 

Forms

This is boilerplate to build a form with 3 fields, two text boxes and a checkbox.
Make a component like 'addTask':
At the top of the file, make a useState hook for each form input:
const [text, setText] = useState(''); const [day, setDay] = useState(''); const [reminder, setReminder] = useState(false);
Remember, the parenthesis contain defaults, thus the form now shows these as values
Then create a form in the return section:
return ( <form className='add-form' onSubmit={onSubmit}> // You'll make the onSubmit method later <div className='form-control'> <label>Task</label> // Label is just a heading above a text box <input type='text' // Text box placeholder='Add Task' value={text} // Later you'll set the state next which this value is showing onChange={(e) => setText(e.target.value)} // e = event. This tells setText (the useState hook above) // to equal the value of the input field /> </div> <div className='form-control'> <label>Date</label> <input type='text' placeholder='Add Date' value={day} onChange={(e) => setDay(e.target.value)} /> </div> <div className='form-control form-control-check'> <label>Set Reminder</label> <input type='checkbox' checked={reminder} // The dummy data we're using gives true or false, we're // just making the box match value={reminder} onChange={(e) => setReminder(e.currentTarget.checked)} // Alternate way to setState on checkbox /> </div> <input type='submit' value='Save Task' className='btn btn-block' /> </form> )
At whatever parent component you're controlling state from (In this case App.js), make a component to console log:
const addTask = (task) => { console.log(task) }
Then pass it down to the component, Note: We called it onAdd:
<AddTask onAdd={addTask}/>
Within the Addtask component make sure to add it in the props parenthesis const AddTask = ({ onAdd }) => {...
Now, to ensure the form is valid, add some sanitisation code that will pass up the values to the parent AND reset the forms for new data:
const onSubmit = (e) => { // e is for event e.preventDefault() // Prevent the page reloading when submitted if (!text || !day) { // Alert user to enter a valid task alert('Please add a task and date') return } onAdd({text, day, reminder}) // Call onAdd from the App.js component setText('') // Reset the fields within this state setDay('') setReminder(false) }
Finally we need to add the tasks within the parent in some way. Here it's a hard coded JSON object but it requires a unique id (It looks like {id: 123, text: 'blah', day: 'blah', reminder: false}).
So we'll need to not only add an id, but add the task to the others AND make it rerender using whatever setState displays our data (In this case is the setTasks in const [tasks, setTasks] = useState([jsonarray]))
Change the addtask like so:
const addTask = (task) => { const id = Math.floor(Math.random() * 10000) + 1 // Good chance of creating a unique id const newTask = {id, ...task } // Make an object wth id andadd the new task (remember its made up of {text, date, reminder}) setTasks([...tasks, newTask]) // Finally, update the state with the old list plus the new task }
Alternatively if you had an async function to update a db that would give you an id AND show it in the UI, it would look like this:
const addTask = async (task) => { const res = await fetch('http:localhost:5000/tasks', { method: 'POST', // Updating the DB headers: { 'Content-type': 'application/json', // Telling the server to expect json }, body: JSON.stringify(task), // Convert json to a string }) console.log(data) const data = await res.json() // The server then sends the post request back (Just this add itself) setTasks([...tasks, data]) // Which we append to the existing data to show in the UI }

Push to Production

Stop the app and run yarn build
This creates a build folder which is the folder you'd actually run the app with, you could delete all the files except build and it'd run
An easy way to get it up and running is to use serve http:
npm install -g serve You want it global as it's a thing that runs, not a dependency, use sudo for mac and linux
Finally, run serve -s build -p 8000 and checkout localhost:8000 :)

Mini Backend Using JSON Server

JSON Server is a great way to run a makeshift backend with very minimal setup.
Run npm install json-server
Then within package.json add a line under "scripts":
"server": "json-server --watch db.json --port 5000"
db.json will be created if it doesn't exist
To do a request we can use the useEffect hook, to ensure its OOP you should make the actual api request it's own method, then call that method with useEffect on whatever component needs it (in this case just App.js)
const [tasks, setTasks] = useState([]) // This is an example state to manipulate useEffect(() => { // useEffect needs it's own arrow function for some reason const getTasks = async () => { // async makes this run on an extra thread const tasksFromServer = await fetchTasks() // Runs fetchTask method (below) and waits for response setTasks(tasksFromServer) // runs the above setState to match the data recieved } getTasks() // This happens first to run const getTasks }, []) const fetchTasks = async () => { const res = await fetch('<http://localhost:5000/tasks>') // Calls the api and waits for a response const data = await res.json() // Converts the response to json and returns it return data }
To delete is very simple, this is a deleteTask method residing in App.js that was already removing a task from a list of tasks in the state so adding a delete ehre makes sense just before the ui is updated:
const deleteTask = async (id) => { // task id already being sent to the method await fetch(`http://localhost:5000/tasks/${id}`, { method: 'DELETE', }) // Ensure using backticks (${id}) setTasks(tasks.filter((task) => task.id !== id)) // This existed };
Finally to update a value is a little strange, we'll need a method to grab the thing we want to update from the db (e.g search task by id and return it srom the server), then we change the value of the json like so:
const updatedTask = {...taskWeJustGot, key: newValue} // idk why we make a copy and store it but we do
then use a PUT to update the server, in this case we're using the id:
const res = await fetch(`http://localhost:5000/tasks/${id}`, { method: 'PUT', headers: { 'Content-type': 'application/json' }, body: JSON.stringify(updatedTask) // here is the value we changes })
Then we ask for a response which we'll use to update the server, alternatively we could get all the data and update state that way but this is more optimal
const data = await res.json()
In this case, we want to .map() over every item in a list of tasks:
setTasks(tasks.map((task) => task.id === id ? { ...task, reminder: data.reminder } : task)) //Make the value of task with the matching id match what we now have in the server, ie data.reminder

Guides and practical notes, training references, and code snippets shared freely for learning and career growth.