published
updated
React uses Components
to do basically everything. Technically (Generally?) Components are Javascript functions[#]_ that return HTML elements. Where it all comes together (if you're using create-react-app
) is the index.js
file which contains this line:
{{{javascript
ReactDOM.createRoot(document.getElementById('root')).render(
Which renders the React Component defined by App
into the root
element of index.html
.
Though it doesn't really matter what's in the App
function, it will matter to us later, so it currently looks like this:
{{{javascript const App = () => { console.log('Hello from component') return (
Hello world
See, it's a function that takes no arguments, using the ECMA6 Arrow definition of a function. It runs javascript, so you can interact with the console, and it returns something approximating HTML.
A detour into JSX
JSX is a syntax extension to Javascript that allows writing HTML-like markup inside a JS file. Although it looks like HTML, it isn't really, it gets compiled down to Javascript by a compiler (Babel
in the case of create-react-app
). JSX is quite similar to HTML, but it allows dynamic execution in HTML elements using the { curly brace }
syntax. Think of it like a templating language, which has a tree like structure similar to HTML. One difference is that every tag must be closed.
For comparison:
{{{html
}}}
{{{jsx
}}}
Props to React
Components can be nested. Components can take data. Components can run functions. It's all components all the way down! You're using Javascript after-all, why wouldn't these things be possible? You can nest components by simply calling the function as a closed JSX tag in the HTML you're returning. But passing data is done by so called "props". Really they're just a parameter that your function takes that contains the data you can manipulate inside the HTML markup, again through the magic of JSX.
Because Javascript is Javascript, convention dictates that you have a single parameter entitled props
which takes an object that defines all the data you want to pass to the function. Let's take a look at the example from up top but with all the additional things we've just learned.
{{{javascript const Hello = (props) => { console.log(props) return (
Hello {props.name}, you are {props.age} years old
const App = () => { const name = 'Peter' const age = 10 return (
Greetings
Here we've defined 2 React Components, Hello
and App
. App nests the Hello component inside it, and passes each a different value for their props. We can both "hard code" values to pass to props, or it can be the output of some javascript execution, but in that case we must wrap it in { curly braces }
. Despite props being a single object being passed, we can add any number of arbitrary fields to it. These fields can then be accessed using javascript and dot notation to extract the exact value from the object to be rendered.
Note: We cannot render an Object! We have to render base types like strings or integers. Objects are not valid as a React child!!
React also allows us a special kind of state, that we update in a functional style. Presumably, there are many ways to manage this state, including redux is what manages the local state when it gets out of hand. (Let's pass around a giant immutable map like in Clojure!~)
State is defined through the help of a React library, useState
. It works like so:
{{{javascript import { useState } from 'react'
const App = () => { const [state, setState] = { name: "Voidrynth", age: 36 } return (
Greetings
Functions and Event Handlers
React Hooks are the style of React that's recommended, and in it, everything is a function. Our state must also be immutable for React to work well(?). This means a Functional Programming style! Exciting :3
To override event handlers we use callbacks, callbacks must be functions. We can override a button like so:
{{{javascript const App = (props) => { const onClick = () => { alert("Hello!") }
return (
<>
<button onClick={onClick}> Knock Knock </button>
</>
)
} }}}
This button has our onClick definition (which is a function!) that gets called on the button's onClick
. This is an event handler, but in this case we don't care about the event, because we assume it can only mean one thing.
Notably when we do this, all the element's functionality gets overridden! If your element does something by default, or as a side effect of interaction, you got to manage that too!
This is particularly important with text boxes:
{{{javascript import { useState } from 'react'
const App = (props) => { const [text, setText] = useState('Type me...')
return (
<>
<input value={text} />
</>
)
} }}}
Now your user won't be able to change the text in this input box, because none of their input functionality remains. Fixing this requires adding back that functionality with our controlled state
{{{javascript import { useState } from 'react'
const App = (props) => { const [text, setText] = useState('Type me...')
const onChange = (event) => {
setState(event.target.value)
}
return (
<>
<input value={text} onChange={onChange}/>
</>
)
} }}}
Now your state will be updated whenever the user types. This is an event callback which also passes the event that caused it.
Finally, despite React over-writing all actions, it cannot stop default actions. So we have to manually intercept those.
{{{javascript const App = (props) => { const onClick = (event) => { event.preventDefault() alert("Hello!") }
return (
<>
<form>
<button type="submit" onClick={onClick}> Knock Knock </button>
</form>
</>
)
} }}}
Normally a form submit would refresh the page and stuff, but we don't want that.
.. [#] Technically JSX, but we'll get into that in a bit