I think of the React callback function as the parent component extending a baseball mitt for a child to throw in the specific details of an instance. Though, Jason Arnold says it better, technically speaking:
I recently added a modal to a React app for an educational start-up. The modal offers users a text field to create a new TypeScript object, and selection of the new or existing objects from the database to display.
The feature looked something like this below (the code is here on CodeSandbox). I spun up a toy program to make sure I understood the process, since it was one of my bigger jobs during a front end development internship.
The user opens the modal, and sees a text field for entering a dog’s name. The steps are highlighted in yellow, extra notes are in blue, with some modal code removed for clarity.
Child: AddDogField.tsx
import { useState } from "react";
import { useFormik } from "formik";
// parent callback function called by props (more on this in the next section)
interface AddDogFieldProps {
onCreateDog: (name: string) => Promise<void>;
}
interface DogFormValues {
dogName: string;
}
export const AddDogField = (props: AddDogFieldProps) => {
// useFormik (good for simple forms) vs Formik, Jared Palmer on GitHub
const formik = useFormik({
// initialValues (here, dogName), tells Formik which key of "values" to update, Formik
initialValues: {
dogName: ""
},
// 2. The Formik onSubmit handler is passed the form value (the new dog's name), along with FormikBag methods and props
onSubmit: async (
// The values are assigned a string type through DogFormValues
values: DogFormValues,
// "either reset the form after submission, or imperatively reset the form," (Form Foundations).
{ resetForm }: { resetForm: () => void }
) => {
if (values.dogName.length > 0) {
// 3. onCreateDog() is a callback function defined in the parent component, DogList, and passed here via props (interface addDogFieldProps, above). More on this in the next section.
await props.onCreateDog(values.dogName);
// examples of FormikBag: methods that begin with set, resetForm, any props passed to wrapped component
resetForm();
}
}
});
return (
<AddDogContainer>
<DogButton onClick={handleClickOpen}>Add dog</DogButton
<Dialog>
<DialogContent>
// 1. The user enters a name, and it triggers handleSubmit()
<form onSubmit={formik.handleSubmit}>
<TextField
id="dogName"
name="dogName"
type="dogName"
variant="standard"
placeholder="Dog's name"
fullWidth
style={{ fontSize: "14px" }}
value={formik.values.dogName}
onChange={formik.handleChange}
/>
<DogButton type="submit" style={{ margin: "10px 0px" }}>
Submit
</DogButton>
</form>
<DogButton onClick={() => handleClose()}>Close</DogButton>
</DialogContent>
</Dialog>
</AddDogContainer>
);
};
Pass data from React child to parent functional component, example
import { useState } from "react";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { AddDogField } from "./AddDogField";
import { DogListContainer, ListContainer } from "./AddDogStyles";
interface Dog {
id: number;
name: string;
}
export const DogList = () => {
const [dogs, setDogs] = useState<Dog[]>([]);
const [selectedDog] = useState<number | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const handleClickOpen = () => {
setModalOpen(true);
};
const handleClose = () => {
setModalOpen(false);
};
// 4. Returns the selected dog's name (the value of <MenuItem value={dog.name}>)
const onSelectDog = (event: any) => {
const selectedDogName = event.target.value;
document.getElementById("showName").innerHTML = selectedDogName;
};
// 1. callback function, onCreateDog(), is passed as a prop to the child component to get data from the child's form (the new dog's name) to update the parent's state. It creates a Dog object using the name, and saves it to "dogs." State variable "setDogs" calls the useState hook, which preserves the list of names between re-renders.
const onCreateDog = async (name: string) => {
const newDog: Dog = { name };
const newDogList = [...dogs, newDog];
setDogs(newDogList);
};
return (
<div>
<DogListContainer>
<ListContainer>
// 3. dogs.map returns a list of all dogs for selection
<Select value={selectedDog} onChange={onSelectDog}>
{dogs.map((dog: Dog) => (
<MenuItem value={dog.name}>{dog.name}</MenuItem>
))}
</Select>
</ListContainer>
// 2. passing the callback function to the child as a prop
<AddDogField
onCreateDog={onCreateDog}
/>
</DogListContainer>
</div>
);
};
As a new React developer with an object oriented mindset, I thought of child components as complete programs plugged into the parent. Objects are powerful, and inheritance is automatic, right? It was hard to wrap my mind around data flow, writing code to pass data between the child and parent, and lifting state without Redux.
Passing data from functional child to parent
Importing a child component, like my <AddDogField /> , is the parent engaging with the child. And, here, the parent passes the onCreateDog callback function to the child as a prop, so the child’s data (the new dog’s name) can ultimately update the parent’s state: “A new Dog(name) is born.”
The callback function lifts state up (React docs) to the closest common ancestor (DogList) between the components that use or change this state (both parent DogList and child AddDogField). So, data is still flowing unidirectionally, a tenant of React, from parent to child, and to the view.
Unidirectional flow in React means that users trigger actions in the view, the actions update state, and changes flow back down to the child components and view. So, it’s generally best to update state in response to a DOM event like onClick, or an effect like useEffect.
I set the wrapper to width: 800px, as a reasonable responsive start size, and the wrapper’s contents to flex-grow: 2; so the slide images grow to fill the wrapper.
<script>
var slideIndex = 1;
showSlides(slideIndex);
function plusSlides(n) {
showSlides(slideIndex += n);
}
function showSlides(n) {
var i;
var slides = document.getElementsByClassName("mySlides");
if (n > slides.length) {slideIndex = 1}
if (n < 1) {slideIndex = slides.length}
for (i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}
slides[slideIndex-1].style.display = "flex";
}
</script>
The script first sets the page default to show slide 1. showSlides is defined below these first two lines of JavaScript, but works outside of regular top-to-bottom flow control because its binding is defined using function declaration Eloquent Javascript.
var slideIndex = 1;
showSlides(slideIndex);
A user’s click on a directional arrow triggers plusSides(n). So, the current slide (slideIndex) is added to n (either 1 or + -1, depending on the arrow clicked). += is an addition assignment operator, which adds an operand to a variable, and updates the variable with the result (MDM, addition assignment).
function plusSlides(n) {
showSlides(slideIndex += n);
}
So, n updates to the new current slide position, and passes as a parameter to showSlides(slideIndex += n).
getElementsByClassName and length property
getElementsByClassName returns a live HTMLcollection object (“slides”) which, here, includes all the child elements with the class name, “mySlides.” The JS length property (w3Schools) returns the number of elements in the object, often used when looping through elements. (More on live vs. static lists from Abi Travers.)
function showSlides(n) {
var i; // declaring variable i (here, still undefined)
var slides = document.getElementsByClassName("mySlides");
if (n > slides.length) {slideIndex = 1}
if (n < 1) {slideIndex = slides.length} // function continues below
if (n > slides.length) {slideIndex = 1} determines, “if the new slide index is greater than the length of the array, restart at position 1.”
if (n < 1) {slideIndex = slides.length} determines, “If the current slide index is less than 1, it’s the last one in the collection.”
JS for loop, display one element in collection
for (i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}
slides[slideIndex-1].style.display = "flex";
}
for ([initialExpression]; [conditionExpression]; [incrementExpression])
statement
for (i = 0; i < slides.length; i++) {
The above code determines, “the counter starts at 0, and checks if the counter is still less than the length of the collection at each pass of the loop. At each pass, the counter increments by 1.”
“For each pass, set the slide with the index of the counter to display: none,” making all slides invisible.
slides[i].style.display = "none";
}
“Make one slide visible (at a time), the slide with the current index.”
For a slightly more complex live version, visit https://sara-harvey.github.io/starling/, with the code on GitHub at https://github.com/Sara-Harvey/starling. If I can do anything to improve this post, send me a DM on Twitter, and let me know. @SaraHarvy
Learning to code takes time, like anything creative
I was hitting errors on a coding project one afternoon in June (2019) like a fly bouncing its face against a window over and over. I stopped when FedEx delivered our new vice grip pliers. We wanted to install the air conditioner in our bedroom, but a child guard blocked the window, and the pliers were our key to cool.
In New York City, you only need the guard if you have children, but these bars are impossible to remove without a special tool. You can’t ask the super to do it, because the bars are “permanent.” I was already sweating over my laptop, so I breaked to tackle the window.
I Googled a refresher YouTube on vice grip pliers. Then, I clamped the pinchers onto the millimeter-tall side of the first screw head, which was the only way to grip it. You could saw the screw heads off the guard bars, according to the Internet, but we wanted to avoid destroying anything. So, I twisted against the rusty threads.
Again and again, and again.
I’m learning to code through the Flatiron School’s online boot camp, and my biggest epiphany so far has been that programming is building in the tiniest increments. You write code, hit enter, the program says, “Error!” and you fix it over and over. Soon, you have a beautiful web app.
You learn how to draw, write, cook or do anything in increments, but coding can make the increment aspect painfully obvious. If you need to fudge a chowder, just add more clams, or corn or Manhattan, but a Sinatra app needs working Ruby code or, “Error!”
I’m learning to be patient with the increment.
That afternoon, I had been programming with the Nokogiri gem, named for a precise woodworking saw, to try to grab the correct HTML elements, over and over, in a web scrape. When the screws on the window budged, then turned until they dropped free into my hand, I went back to my code encouraged that miracles really do happen.
Though, I am pretty handy. When I was 10 or 11, I liked sawing boards and logs with my dad’s old hand saw because I knew that if I was just patient, I could divide any castoff thing in the woodpile clean in half. Right after I sawed through the knee of my jeans. (This is how I played. Is that weird?)
If you’re learning something hard, coding, spoken Mandarin, chowder-craft, amateur carpentry, have faith. Increments build.
The @NewNewRomBot1 is a Ruby Twitter bot that mashes up lyrics from Duran Duran, Spandau Ballet, Boy George, Flock of Seagulls and other acts from the New Romantic/New Wave early 80s. So far, it involves 100 songs.