tl;dr
Functions are about inputs, side effects, and outputs. React component functions are no different. How do you think about props, state, and context in terms of inputs, side effects, and outputs? What follows is a model I find useful in talking with students. I'd love to hear how it sits with you, or if it brings up any questions!
Intro
With the latest versions of React (>16.8), it's possible to model all aspects of a client application as a problem of functions and composing functions. Hooks provide a basic functional abstraction for state, side effects outside of the render cycle, and retrieving data from various contexts in the React tree.
Functions are a set of inputs, side effects, and an output. React introduces many new ideas, but they still map well to more basic ideas about how functions work. Let's take a look at how that mapping looks!
Props: Direct Input
Props are the direct inputs into a function. They are passed in React together as an object argument to the component function.
Here we see an example of a "Score" display in Typescript that takes a number
prop called score
. It renders that score into a string, and that string into a span
element. Eventually, our inputs will be represented in some form in the output of our combined component functions, and in the rendered result!
// score.tsx
import React from 'react'
export function Score({ score: number }) {
return <span>Your score is {score}</span>
}
Context: Indirect Input
Context is another available input into our React component functions. Where as props are direct, context is an indirect way to pass data to our components. With context, we pass a value once as a prop to a provider above our components in the React tree. This provider then, through React, passes the value to its consumers within our components.
Here is an example in Typescript along the same lines, with a GameHeader
pulling a Game
object from the context, and rendering a the score. Notice how the same value passes from context, and becomes a prop. We'll see how to update this value later when talking about state!
// game.tsx
import React from 'react'
export type Game = { score: number }
export const GameContext = React.createContext<Game>({ score: 0 })
// game_header.tsx
import React from 'react'
import Score from './score'
import {GameContext} from './game'
function GameHeader() {
const { score } = React.useContext(GameContext);
return <header><Score score={score} /></header>
}
State: Redirect Input
Finally, we have state. State is also an input, but it is also a side effect. That's why the hooks for state, useState
and useReducer
, both return a tuple of a value and a function. The value is the input, and the function performs the side effect.
Hint: When you see a function that doesn't return a value (in Typescript something like
() => void
) chances are it performs a side effect. That's not to say that functions that return values can't perform side effects, but those that don't most likely either do nothing or perform a side effect.
The side effect in this case triggers the component to re-render and receive the updated value. This allows you to redirect values within your application. Here we see a more complete example, where the score
is stored, and updated each time the user clicks a button
:
// app.tsx
import React from 'react'
import {GameContext} from './game'
import {GameHeader} from './game_header'
function App() {
const [game, incrementScore] = React.useReducer(({ score }) => ({
score: score + 1
}), { score: 0 });
return (
<GameContext.provider value={game}>
<GameHeader />
<button onClick={() => incrementScore()}>Click Here!</button>
</GameContext.provider>
)
}
Notice how the side effect function ends up composed into our output (in the onClick
handler) along with the value. That side effect redirects the click event into our stateful value, and re-inputs that into our component tree. Cool!
So how does this fit into your mental framework? I'd love to hear!
Top comments (3)
First of all, you summarized your question beautifully and easy to understand - amazing job.
The way I code has drastically changed depending on how much I understand the full power and optimization methods of React Hooks.
Here is how it ends up after refactoring.
Props -> barely ever use them unless it's sent one level down. This is probably because of the nature of the projects I've been doing recently, but most data is stuffed in redux, and sent to each component directly via useSelector hook(react-redux)
Context -> won't even attempt to use it before debugging becomes as easy as redux.
State -> usually spam like variables depending on the deadline of a project, but eventually gets refactored into useReducer or a useState that pretty much acts like a useReducer.
That's pretty much it. :)
That's really interesting! Especially the insight about the number of levels down which the props are propagated, I can definitely see how that pattern arises as well.
What do you think is the difference in your projects between what state ends up stored in Redux and what makes it into Hooks state?
The one change I would make to your summary is that you are using context I would bet internally via
useSelector
, so I would say instead that you "won't attempt to use context without the debugging capabilities of the redux store." That is a point of view about the relation between Redux and context I hadn't considered and is very interesting as well!What do you think is the difference in your projects between what state ends up stored in Redux and what makes it into Hooks state?
-> The most recent project I worked extensively on was a React-redux + Electron + Sqlite Chat app targeted for Windows. In order to reduce server load, previously loaded data was saved to Sqlite and loaded when the app started, new data on initial load was acquired via REST and sent to redux, along with real-time data received via SocketIO. Since data had to be acquired real-time and updated to the main Electron window's redux store, and also sent to other Electron windows via IPC, there was little to no benefit to keeping data in a component's state - it was too versatile and everything affected everything else so frequently.
In hindsight, an MVVM project structure would have reduced a lot of hassle but we all know that switching project structure is close to impossible when you're barely able to add features on time.
So to answer the question, I'd say there are so many factors that could possibly affect where you store the data, but then again I would consider a chat application as a rather rare edge case.
In most websites, I'd say using redux the least possible is the best-case scenario.
you are using context I would bet internally via useSelector
-> I agree, I was pointing out that the Context API itself currently cannot compete against Redux's extensive debugging capabilities, in the functional sense, those two are virtually the same in most aspects. I believe there are quite a few posts on dev.to on the subject that point this out.