Full Stack React Notes
Intro to React
Exercise 1.1-1.2
Some personal notes from these exercises
Breaking down each section Starting with the App component, we have a course object, and looking at the return statement, lets find out the overall strucuture of the App. Returning a Header, Content and Total Parts components. Each one taking in a prop.
Header component displays object course name, half statck app dev.
Part component takes in props, and returns a name and the exercise.
I want to retake this course for a week or so, in order to learn better test driven development and refine my understanding with React.
Component state, event handlers
Destructuring
Reference Link (opens in a new tab)
Allows us to take values from objects and arrays and destructure them uppon assignment.
const Hello = (props) => {
const { name, age } = props
}
Taking a step further
const Hello = ({name, age}) => {
Page re-Rendering
Stateful components
const [counter, setCounter] = useState(0)
The function call adds state to the component and it is rendered with the initial value of zero.
Event Handling
Introduction to event handling via user interaction.
An event handler is a function
Why do we write our event handlers like this?
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
And why wouldnt we write it like this?
<button onClick={setCounter(counter + 1)}>
plus
</button>
The above code would break the application, an event handler is supposed to be either a fucntion or a function reference. It is a function call, when the website is first launched the counter variable is 0. When the component is rendered the counter variable is 0, and when it is called 0+1 would equal 1 causing the page to rerender and call the function again.
Typically defining event handlers within the JSX-tempaltes is not a good idea!
Seperate the event handlers in to seperate functions like so:
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1)
const setToZero = () => setCounter(0)
return (
<div>
<div>{counter}</div>
<button onClick={increaseByOne}>
plus
</button>
<button onClick={setToZero}>
zero
</button>
</div>
)
}
Here, the value of the onClick attribute is a variable containing reference to a function.
Passing state - to child components
It is recommended to write React components that are small and reusable across applications and even across projects. Refactoring our application so that it is composed of 3 smaller components:
- One component for displaying the counter
- Two components for buttons
Implementing a display component that is responsible for displaying the value of the counter.
A good practice in React is to lift the state up in the component hierarchy.
Sharing States Between Components (opens in a new tab)
In practice we place the application's state in the App component and pass it down to the Display component via props:
const Display = (props) => {
return (
<div>{props.counter}</div>
)
}
To use the component we need to only pass the state of the counter to it:
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1)
const setToZero = () => setCounter(0)
return (
<div>
<Display counter={counter}/>
<button onClick={increaseByOne}>
plus
</button>
<button onClick={setToZero}>
zero
</button>
</div>
)
}
When the bottons are clicked App get re-rendered, all of its children get re-rendered as well.
The next step would be to make a Button component for all the buttons in our application. Think of what we have to pass with the component...?
The event handler and the type/title of the button through the component's props:
const Button = (props) => {
return (
<button onClick={props.handleClick}>
{props.text}
</button>
)
}
The App component now looks like this
const App = () => {
const [ counter, setCounter ] = useState(0)
const increaseByOne = () => setCounter(counter + 1)
const decreaseByOne = () => setCounter(counter - 1)
const setToZero = () => setCounter(0)
return (
<div>
<Display counter={counter}/>
<Button
handleClick={increaseByOne}
text='plus'
/>
<Button
handleClick={setToZero}
text='zero'
/>
<Button
handleClick={decreaseByOne}
text='minus'
/>
</div>
)
}
Now the component button is being reused and has multiple functions.
Changes in state cause rerendering
Going over the main principles on how the application works once more.
How does this work in your own words?
- We have a App component that is rendered
- In that component we have a useState hook, called counter.
- Counter is set with an initial state of 0
- In this section it is common practice to use object destructuring
- We have 3 functions, to inc, dec and set to zero
- we return a div containing all elements and
- a Display component, and 3 Button components
- The display component takes in a prop counter
- 3 Button compponents, pass in a handleClick prop, to handle the event and a text.
- Each button event causes a re-render and changes state
Adding console logs helps understand how the code flows
const App = () => {
const [counter, setCounter] = useState(0)
console.log('rendering with counter value', counter)
const increaseByOne = () => {
console.log('increasing, value before', counter)
setCounter(counter + 1)
}
const decreaseByOne = () => {
console.log('decreasing, value before', counter)
setCounter(counter - 1)
}
const setToZero = () => {
console.log('resetting to zero, value before', counter)
setCounter(0)
}
return (
<div>
<Display counter={counter} />
<Button handleClick={increaseByOne} text="plus" />
<Button handleClick={setToZero} text="zero" />
<Button handleClick={decreaseByOne} text="minus" />
</div>
)
}
Refactoring the components
After this section, try building this out by yourself, use an existing project or creat a whole new one.
The component displaying the value of the counter is as follows:
const Display = (props) => {
return(
<div>{props.counter}</div>
)
}
Since the component only uses the counter field of its props, we can destructure the component and it simplifies as:
const Display = ({ counter }) => {
return (
<div>{counter}</div>
)
}
Since the function defining the component contains only the return statement we can further refactor it as such
const Display = ({ counter }) => <div>{counter}</div>
Simplyfying the button component
const Button = (props) => {
return (
<button onClick={props.handleClick}>
{props.text}
</button>
)
}
Destructuring the button component
A more complex state, debugging React applications
Complex State
What if our application requires a more complex state? In most cases the best way to accomplish this is using the useState function to create separate "pieces" of state.
Here is another example in code, where we have two peices of state for the application named left and right, that both get the initial value of 0:
const App = () => {
const [left, setLeft] = useState(0)
const [right, setRight] = useState(0)
return (
<div>
{left}
<button onClick={() => setLeft(left + 1)}>
left
</button>
<button onClick={() => setRight(right + 1)}>
right
</button>
{right}
</div>
)
}
First thing I notice is the redunancy, a function that takes input left or right and increments by 1.
The components state can be of any type, we can further simplify things, by saving the left and right buttons in to a single object. And the application would look like this.
const App = () => {
const [clicks, setClicks] = useState({
left: 0, right: 0
})
const handleLeftClick = () => {
const newClicks = {
left: clicks.left + 1,
right: clicks.right
}
setClicks(newClicks)
}
const handleRightClick = () => {
const newClicks = {
left: clicks.left,
right: clicks.right + 1
}
setClicks(newClicks)
}
return (
<div>
{clicks.left}
<button onClick={handleLeftClick}>left</button>
<button onClick={handleRightClick}>right</button>
{clicks.right}
</div>
)
}
We can simplify this even further by using object spread syntax. Spread syntax (opens in a new tab)
const handleLeftClick = () => {
const newClicks = {
...clicks,
left: clicks.left + 1
}
setClicks(newClicks)
}
const handleRightClick = () => {
const newClicks = {
...clicks,
right: clicks.right + 1
}
setClicks(newClicks)
}
In practice ... clicks creates a new object that has copies of all properties of the clicks object.
...clicks, right: clicks.right + 1
This creates a copy of the clicks object where the vlaue of right property is increased by one.
Assigning the object above to a variable in the event handler is not necessary and we can simply the functions
const handleLeftClick = () =>
setClicks({ ...clicks, left: clicks.left + 1 })
const handleRightClick = () =>
setClicks({ ...clicks, right: clicks.right + 1 })
Also it is important to note that we should not import state directly Changing state should always be done by setting state to a new object. In this case if prop from the previous state are not changed, all is needed is to be copied.
Handling arrays
Adding a piece of state in the application to contain an array allClicks that remembers every click that has occured in the application.
const App = () => {
const [left, setLeft] = useState(0)
const [right, setRight] = useState(0)
const [allClicks, setAll] = useState([])
const handleLeftClick = () => {
setAll(allClicks.concat('L'))
setLeft(left + 1)
}
const handleRightClick = () => {
setAll(allClicks.concat('R'))
setRight(right + 1)
}
return (
<div>
{left}
<button onClick={handleLeftClick}>left</button>
<button onClick={handleRightClick}>right</button>
{right}
<p>{allClicks.join(' ')}</p>
</div>
)
}
Every click is stored in a seperate piece of state called allClicks, which is initialized as an empty array.
When the left or right button is clicked we add the L or R to the allClicks array. It is important to remember that state allClicks must not be mutated directly.
Looking more closely at how the clicking is being render on the page:
const App = () => {
// ...
return (
<div>
{left}
<button onClick={handleLeftClick}>left</button>
<button onClick={handleRightClick}>right</button>
{right}
<p>{allClicks.join(' ')}</p>
</div>
)
}
Update of the state is asynchronous
State updates in React asynchronously, not immediately but at some point, before the component is rendered again.
Conditional Rendering
Go over example more covered in part 2
Deubgging React Applications
A large part of a devs time is spent on debugging/reading existing code.
Important rules of web development
Keep the browser dev console open at all times. Dont write more code but rather find and fix the problem immediately.
Rules of Hooks
Hooks may only be called from inside the function body that defines a React component
Event Handling
In this section we cover how we handle events. Event handlers must always be a function or a reference to a function.
<button onClick={() => console.log('clicked the button')}>
button
</button>
When the component get rendered, no function gets called and only the reference to the arrow fucntion is set to the event handler. The calling of the function only happens once the button is clicked.
Often we will even see event handlers defined in a seperate place like such.
const App = () => {
const [value, setValue] = useState(10)
const handleClick = () =>
console.log('clicked the button')
return (
<div>
{value}
<button onClick={handleClick}>button</button>
</div>
)
}
A function that returns a function
Lets re-write this ecent handler another way, buy using a function that returns a function
const App = () {
const [value, setValue] = useState(10)
const hello = () => {
const handler = () => console.log('hello world'
return handler
)
}
return (
<div>
{value}
<button onClick={hello()}></button>
</div>
)
}
Earlier we stated that an event handler may not be a call to a function and that it has to be a function or a reference to a funciton. Why does this work then?
It is because how the function hello is defined!
const hello = () => {
const handler = () => console.log('hello world')
return handler
}
The return value of the function is another function.
Why or what is the point of this concept?
From my understanding, it allows us to accept prameters in event handlers. Because defining a function that handles another function and quite redundant. This way it allows us to streamline the process in a more efficent way. We don't want the component to run the function right when it renders, we want the function to execute only when, we want it to.
Changing the code a little bit:
const App = () => {
const [value, setValue] = useState(10)
const hello = (who) => {
const handler = () => {
console.log('hello', who)
}
return handler
}
return (
<div>
{value}
<button onClick={hello('world')}>button</button>
<button onClick={hello('react')}>button</button>
<button onClick={hello('function')}>button</button>
</div>
)
}
Now there are three buttons with the event handlers defined by the hello function that accepts a parameter.
The event handler is created by executing the function call hello('world'). The function call returns the function:
() => {
console.log('hello','world')
}
Functions returning functions can be utilized in defining generic functionallity that can be customized with parameters. The hello funciton that creates the event handler can be thought as a factory that produces customized event handlers meant for greeting users.
Simplyfying our definition a bit:
const hello = (who) => {
const handler = () => {
console.log('hello', who)
}
return handler
}
Eliminating the helper variables and directly return the created function:
const hello = (who) => {
return () => {
console.log('hello', who)
}
}
The hello function is composed of a single return command, so we can omit the curly brases and use the more compact syntax for arrow functions
const hello = (who) =>
() => {
console.log('hello', who)
}
Lastly we should right all of the arrows on the same line
const hello = (who) => () => {
console.log('hello', who)
}
Personally I do not think this looks as good, it does not provide great readability.
However we can define the eventhandlers that set the state of the component to a given value like so
const App = () => {
const [value, setValue] = useState(10)
const setToValue = (newValue) => () => {
console.log('value now', newValue) // print the new value to console
setValue(newValue)
}
return (
<div>
{value}
<button onClick={setToValue(1000)}>thousand</button>
<button onClick={setToValue(0)}>reset</button>
<button onClick={setToValue(value + 1)}>increment</button>
</div>
)
}
Using functions that return functions is not require to achive this functionality. Another way is to return the setToValue function which is responsible for updating state into a normal function:
const App = () => {
const [value, setValue] = useState(10)
const setToValue = (newValue) => {
console.log('value now', newValue)
setValue(newValue)
}
return (
<div>
{value}
<button onClick={() => setToValue(1000)}>
thousand
</button>
<button onClick={() => setToValue(0)}>
reset
</button>
<button onClick={() => setToValue(value + 1)}>
increment
</button>
</div>
)
}
This on the other hand provides better readability. The helper function is not convoluted with as many arrow functions
Passing Event Handlers to Child Componets
Lets extra the button and create it into its own component
const Button = (props) => (
<button onClick={props.handleClick}>
{props.text}
</button>
)
The component get the event handler function from the handleClick prop, and the text of the button from the text prop.
Using the new component looks like this
const App = (props) => {
// ...
return (
<div>
{value}
<Button handleClick={() => setToValue(1000)} text="thousand" />
<Button handleClick={() => setToValue(0)} text="reset" />
<Button handleClick={() => setToValue(value + 1)} text="increment" />
</div>
)
}
Do Not Define Components Within Components
Adding display component
// This is the right place to define a component
const Button = (props) => (
<button onClick={props.handleClick}>
{props.text}
</button>
)
const App = () => {
const [value, setValue] = useState(10)
const setToValue = newValue => {
console.log('value now', newValue)
setValue(newValue)
}
// Do not define components inside another component
const Display = props => <div>{props.value}</div>
return (
<div>
<Display value={value} />
<Button handleClick={() => setToValue(1000)} text="thousand" />
<Button handleClick={() => setToValue(0)} text="reset" />
<Button handleClick={() => setToValue(value + 1)} text="increment" />
</div>
)
}
This does work, but don't implement components like this! Never define components inside other components. This method provides no benefits and leads to mayn unpleasant problems. React treats a component defined inside of another component as a new component in every render. Which makes it impossible for React to optimize the component. Instead move the Display component function to its correct place, outside of the App component function. Now this can be in a seperate file completely if the app is to complex, but in this case this will work just fine.
const Display = props => <div>{props.value}</div>
const Button = (props) => (
<button onClick={props.handleClick}>
{props.text}
</button>
)
const App = () => {
const [value, setValue] = useState(10)
const setToValue = newValue => {
console.log('value now', newValue)
setValue(newValue)
}
return (
<div>
<Display value={value} />
<Button handleClick={() => setToValue(1000)} text="thousand" />
<Button handleClick={() => setToValue(0)} text="reset" />
<Button handleClick={() => setToValue(value + 1)} text="increment" />
</div>
)
}
Web Programmers Oath
Programming is hard, that is why I will use all the possible means to make it easier
- I will have the browser dev console open at all time
- I progerss with small steps
- I will write lots of console.log statements to make sure I understand how the code behaves and to help pinpoint problems
- If my code does not work, I will not write more code. instead I start deleting the code untill it works or just returns to a state where everything was still working