Skip to content

Commit 2c5dd33

Browse files
committed
Add new robust javascript section
1 parent 6f020f5 commit 2c5dd33

File tree

13 files changed

+141
-4
lines changed

13 files changed

+141
-4
lines changed

lib/components_guide_web/controllers/react_typescript_controller.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule ComponentsGuideWeb.ReactTypescriptController do
1111
"testing" => %{title: "Testing React"},
1212
"forms" => %{title: "Creating Forms in React"},
1313
"reducer-patterns" => %{title: "React Reducer Patterns"},
14+
"zero-hook-dependencies" => %{title: "Zero Hook Dependencies"},
1415
"hooks-concurrent-world" => %{title: "React Hooks in a Concurrent World"},
1516
"logical-clocks" => %{title: "Logical Clocks in React"},
1617
"editor" => %{title: "React Online Editor"},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule ComponentsGuideWeb.RobustJavascriptInteractivityController do
2+
use ComponentsGuideWeb, :controller
3+
require Logger
4+
5+
def index(conn, _params) do
6+
render(conn, "index.html", article: "intro")
7+
end
8+
9+
@articles ["idempotent-javascript-operations"]
10+
11+
def show(conn, %{"id" => article}) when article in @articles do
12+
render(conn, "index.html", article: article)
13+
end
14+
15+
def show(conn, _params) do
16+
raise Phoenix.Router.NoRouteError, conn: conn, router: ComponentsGuideWeb.Router
17+
end
18+
end
19+
20+
defmodule ComponentsGuideWeb.RobustJavascriptInteractivityView do
21+
use ComponentsGuideWeb, :view
22+
use ComponentsGuideWeb.Snippets
23+
alias ComponentsGuideWeb.ThemeView
24+
25+
def header_styles() do
26+
ThemeView.banner_styles(:web_standards)
27+
end
28+
end

lib/components_guide_web/router.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ defmodule ComponentsGuideWeb.Router do
4646
only: [:index, :show]
4747
)
4848

49+
resources(
50+
"/robust-javascript",
51+
RobustJavascriptInteractivityController,
52+
only: [:index, :show]
53+
)
54+
4955
resources(
5056
"/graphics",
5157
GraphicsController,

lib/components_guide_web/templates/landing/index.html.eex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ subhead = "Get better at accessibility, TDD, BDD, naming, performance, and the l
4949

5050
<%= subject_banner(:accessibility_first) %>
5151
<%= subject_banner(:react_typescript) %>
52+
<%= subject_banner(:robust_javascript_interactivity) %>
5253
<%= subject_banner(:web_standards) %>
5354
<%= subject_banner(:composable_systems) %>
5455

lib/components_guide_web/templates/react_typescript/_top.html.eex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<li><%= link("Fundamentals", to: '/react+typescript') %>
1313
<li><%= link("Testing", to: '/react+typescript/testing') %>
1414
<li><%= link("Reducer Patterns", to: '/react+typescript/reducer-patterns') %>
15+
<li><%= link("Zero Hook Dependencies", to: '/react+typescript/zero-hook-dependencies') %>
1516
<li><%= link("Hooks in a Concurrent World", to: '/react+typescript/hooks-concurrent-world') %>
1617
<li hidden><%= link("Event Handlers", to: '/react+typescript/event-handlers') %>
1718
<li><%= link("Logical Clocks", to: '/react+typescript/logical-clocks') %>

lib/components_guide_web/templates/react_typescript/zero-hook-dependencies.html.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Zero Hook Dependencies
2-
# Opinionated React Hooks
1+
# The pain of useEffect’s dependencies
2+
3+
`useEffect` dependencies considered painful.
34

45
## Don’t pass dependencies to `useEffect`
56

@@ -20,4 +21,61 @@ useEffect(() => {
2021
});
2122
```
2223

24+
Summary:
25+
- Change your React effects [to be idempotent](/robust-javascript/idempotent-javascript-operations), which means they can be called as multiple times safely.
26+
- Adding a dependency means both 1. I want to read this value 2. I want to run *when* this value changes. It’s conflating two responsibilities. Removing the dependencies means you can read *any* value, and the when is taken care of with idempotency.
27+
28+
It can seem like hook dependencies are similar to passed props in React: you simply declare what you want to use, and React works out what has changed and skips work if there are no changes. However, there’s one huge difference. React’s changes to the DOM are inert: if React accidentally set a DOM element’s attribute to the same value it already has, there’s no behaviour difference. It’s just slightly inefficient. But if React calls your effect more times than you expected, then that’s changing behaviour. (That’s why the StrictMode double firing effects has been so disruptive) What we have to do is make it as safe as setting an attribute, and we can do that with idempotency.
29+
30+
## Example: autosave
31+
32+
Before:
33+
34+
- `useEffect` has 3 dependencies.
35+
- React will diff dependencies to see what has changed.
36+
- _May_ run after committing if any of the 3 dependencies change.
37+
38+
```js
39+
const fetcher = useFetcher();
40+
const queryToSave = useDebouncedMemo(() => query.clock === 0 ? null : query, 1000, [query]);
41+
useEffect(() => {
42+
if (queryToSave === null) {
43+
return;
44+
}
45+
46+
fetcher.submit(queryToSave.searchParams, { method: "post" })
47+
}, [fetcher.submit, projectID, queryToSave]);
48+
```
49+
50+
After:
51+
52+
- `useEffect` has zero dependencies.
53+
- Runs after committing.
54+
- We do our own check to see what has changed, and whether we should bail or procede.
55+
56+
```js
57+
const fetcherLastValues = new WeakMap();
58+
59+
const fetcher = useFetcher();
60+
useEffect(() => {
61+
if (queryToSave === null) {
62+
return;
63+
}
64+
65+
const key = queryToSave.searchParams.toString();
66+
if (key === fetcherLastValues.get(fetcher)) {
67+
return;
68+
}
69+
fetcherLastValues.set(fetcher, key);
70+
71+
fetcher.submit(queryToSave.searchParams, { method: "post" })
72+
});
73+
```
74+
75+
## Bonus: what if it errors?
76+
77+
## Bonus: aborting earlier requests
78+
79+
80+
2381
## Don’t `useRef` for state
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<header style="<%= header_styles() %>">
2+
<%= render ComponentsGuideWeb.RobustJavascriptInteractivityView, "_top.html" %>
3+
</header>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<nav class="pt-6 pb-4">
2+
<ul y-y x-x=md class="text-lg font-bold text-shadow" data-links="p-3">
3+
<li><%= link("What goes wrong?", to: '/robust-javascript') %>
4+
<li><%= link("Idempotency", to: '/robust-javascript/idempotent-javascript-operations') %>
5+
</ul>
6+
</nav>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div class="mx-auto max-w-4xl text-white">
2+
<h1 y-y x-x=md class="pt-8 row space-x-4 text-4xl text-center font-bold leading-tight text-shadow">
3+
<span class="mr-1 text-5xl">🫵🏾🔨</span>
4+
<span><%= "Robust JavaScript Interactivity" %></span>
5+
</h1>
6+
<%= render ComponentsGuideWeb.RobustJavascriptInteractivityView, "_nav.html" %>
7+
</div>

lib/components_guide_web/templates/web_standards/idempotent-javascript-operations.html.md renamed to lib/components_guide_web/templates/robust_javascript_interactivity/idempotent-javascript-operations.html.md

File renamed without changes.

0 commit comments

Comments
 (0)