From dc00201319b7eeac4b231ae389c49130a8305a28 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 5 Sep 2025 17:35:17 +1000 Subject: [PATCH 01/22] Encapsulate state --- .../encapsulate_as_much_state_as_possible.mdx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/routes/drafts/encapsulate_as_much_state_as_possible.mdx diff --git a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx new file mode 100644 index 00000000..d6a7d424 --- /dev/null +++ b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx @@ -0,0 +1,71 @@ +A common issue I see in frontend codebases, is we have have a component with an interface like this: + +``` + + + +type AutocompleteProps = { + + searchValue: string; + onChangeSearchValue: (str: string) => void; + + selectedValue: T: + onChangeSelectedValue: (value: T) => void; + + renderItem: (value: T) => React.ReactNode; + + isLoading: boolean; + availableOptions: Array; + +} +``` + +There's a sense that components with an interface like this would be easy to test, we could write something like: + +``` +[tests] +``` + +It might be desirable to to write examples like this for Storybook, where we _just_ want to see the component as it is loading. + +## Components like this are a pain + +Components like this are tedious to use. + +The problem with components like these is that the actual important functionality of the component has been left to the consumer. + +In order to use one of these components, we would have to write some code like this: + +``` +EXAMPLE +``` + +🤮 + +Imagine we had a form that had three such inputs! + +Note also that such + +## Instead, encapusulate as much state as you can in your component. + +The better interface is one that looks like this: + +``` +[example] +``` + +This allows the component to do a lot more the heavy lifting for you - "Hey, you give the async function, and I'll take care of all of the internal state for you". + +Some example tests would look like this: + +``` +example +``` + +These kinds of tests make far more sense to me, where each one tells a story. + +## There's nothing say that you couldn't do both + +Technically what we could do is do something like expose these components with less functionality as `AutocompleteStateless` and then use this component in our `Autocomplete` component that then provides the the functionality. + +I'm not sure how much value this would actually have, and I argue that it's the functioning `Autocomplete` component that is the important one. \ No newline at end of file From 43f3c9c8509502faabadc1fc8bda071f3ab9d174 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Mon, 8 Sep 2025 21:59:08 +1000 Subject: [PATCH 02/22] More content --- .../encapsulate_as_much_state_as_possible.mdx | 82 ++++++++++++------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx index d6a7d424..77722953 100644 --- a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx @@ -1,32 +1,33 @@ -A common issue I see in frontend codebases, is we have have a component with an interface like this: +--- +meta: + title: Encapsulate as much state as possible in your component + description: A common pattern I see is a passing every bit of a component's state in as a prop. This just puts the responsibility of that component's actual useful logic on to the parent. + dateCreated: 2025-09-08 +--- +import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; +import { AutocompleteA } from "example-react-autocomplete"; -``` +A common issue I see in frontend codebases, is we have have a component with an interface like this: + +Whereby every bit of state that component could be in, is controlled by the parent and passed in as props. -type AutocompleteProps = { +Note that the decision to use an autocomplete component as the example is an intentional one - autocompletes are non-trivial - they can have issues with debouncing the live search, and out-of-order requests. - searchValue: string; - onChangeSearchValue: (str: string) => void; +There's a sense that components with an interface like this would be easy to test, we could write something like: - selectedValue: T: - onChangeSelectedValue: (value: T) => void; +It might be desirable to to write examples like this for Storybook, where we _just_ want to see the component as it is loading. - renderItem: (value: T) => React.ReactNode; - - isLoading: boolean; - availableOptions: Array; +But these tests do not test one of the key aspects of what we are interested in here - the state transitions. -} -``` +We could start doing tests like this: -There's a sense that components with an interface like this would be easy to test, we could write something like: + -``` -[tests] -``` +but this really is not helpful as a test. -It might be desirable to to write examples like this for Storybook, where we _just_ want to see the component as it is loading. +Here in our test we're just asserting that the parent component correctly transition the prop changes on `isLoading` and `availableOptions` - we can't be sure that they're going to get that right. ## Components like this are a pain @@ -36,36 +37,55 @@ The problem with components like these is that the actual important functionalit In order to use one of these components, we would have to write some code like this: -``` -EXAMPLE -``` + 🤮 Imagine we had a form that had three such inputs! -Note also that such - ## Instead, encapusulate as much state as you can in your component. The better interface is one that looks like this: -``` -[example] -``` + This allows the component to do a lot more the heavy lifting for you - "Hey, you give the async function, and I'll take care of all of the internal state for you". Some example tests would look like this: -``` -example -``` + -These kinds of tests make far more sense to me, where each one tells a story. +These tests tell a story, they're a lot more useful as a test. ## There's nothing say that you couldn't do both Technically what we could do is do something like expose these components with less functionality as `AutocompleteStateless` and then use this component in our `Autocomplete` component that then provides the the functionality. -I'm not sure how much value this would actually have, and I argue that it's the functioning `Autocomplete` component that is the important one. \ No newline at end of file +I'm not sure how much value this would actually have, and I argue that it's the functioning `Autocomplete` component that is the important one. + +## Why is this pattern so common? + +I suspect a big part of the reason that this pattern is so prevalent, is because of tools like Tanstack Query, which provide an interface that, to simplify, looks like this: + +```typescript +type LoadingResult = { + data: T; + state: "success" +}| { + data: null; + state: "pending" | "error" | "loading" +} +``` + +that is, they're not providing async functions as their primary means of interface. + +When developers see that their means for fetching data looks like this, then they're going to tend to reproduce that interface onto the components they're creating. + +RTKQ does helpfully provide a [useLazyQuery](https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#uselazyquery) hook which lends itself more to the style that I've advocate. + +You can see [this discussion on TanStack's Github](https://github.com/TanStack/query/discussions/1205) the basic answer to why a lazy/imperative function is not provided, is because it would be trivial for someone to implement themselves. + +This isn't at all to say that tools like Tanstack are bad, it's just that there is a case to be made for using async functions. Async function quite handily encapsulate the state of a query, as well as the data, all in one handy object. Tools like Tanstack still have their place in that they provide query duplication/caching. + + + From f997cc46361f35038f30a82117b38948bb452b7d Mon Sep 17 00:00:00 2001 From: David Johnston Date: Sun, 19 Oct 2025 15:31:38 +1100 Subject: [PATCH 03/22] More content --- src/demos/encapsulate-state/SpecialButton.tsx | 20 ++++++ .../encapsulate-state/SpecialButton2.tsx | 30 +++++++++ .../encapsulate-state/special-button.css | 29 +++++++++ .../encapsulate_as_much_state_as_possible.mdx | 62 +++++++++++-------- 4 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 src/demos/encapsulate-state/SpecialButton.tsx create mode 100644 src/demos/encapsulate-state/SpecialButton2.tsx create mode 100644 src/demos/encapsulate-state/special-button.css diff --git a/src/demos/encapsulate-state/SpecialButton.tsx b/src/demos/encapsulate-state/SpecialButton.tsx new file mode 100644 index 00000000..7f2efc11 --- /dev/null +++ b/src/demos/encapsulate-state/SpecialButton.tsx @@ -0,0 +1,20 @@ +"use client" + +import './special-button.css'; + +type SpecialButtonProps = { + onClick: () => void; + state: "loading" | "error" | "success" | "pending"; +}; + +export function SpecialButton(props: SpecialButtonProps) { + + + + return +} \ No newline at end of file diff --git a/src/demos/encapsulate-state/SpecialButton2.tsx b/src/demos/encapsulate-state/SpecialButton2.tsx new file mode 100644 index 00000000..98d1f5a9 --- /dev/null +++ b/src/demos/encapsulate-state/SpecialButton2.tsx @@ -0,0 +1,30 @@ + +"use client" + +import './special-button.css'; +import { useState } from 'react'; + +type SpecialButtonProps = { + onClick: () => Promise<{ success: boolean }>; +}; + +export function SpecialButton2(props: SpecialButtonProps) { + const [state, setState] = useState<"loading" | "error" | "success" | "pending">("pending"); + + const handleClick = async () => { + setState("loading"); + try { + const result = await props.onClick(); + setState(result.success ? "success" : "error"); + } catch (error) { + setState("error"); + } + }; + + return +} \ No newline at end of file diff --git a/src/demos/encapsulate-state/special-button.css b/src/demos/encapsulate-state/special-button.css new file mode 100644 index 00000000..e87485b5 --- /dev/null +++ b/src/demos/encapsulate-state/special-button.css @@ -0,0 +1,29 @@ +.special-button { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s ease; +} + +.special-button.pending { + background-color: #007bff; + color: white; +} + +.special-button.loading { + background-color: #6c757d; + color: white; + cursor: not-allowed; +} + +.special-button.success { + background-color: #28a745; + color: white; +} + +.special-button.error { + background-color: #dc3545; + color: white; +} \ No newline at end of file diff --git a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx index 77722953..ea5aab45 100644 --- a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx @@ -5,15 +5,29 @@ meta: dateCreated: 2025-09-08 --- import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; -import { AutocompleteA } from "example-react-autocomplete"; +import { SpecialButton2 } from "@/demos/encapsulate-state/SpecialButton2"; +import { DemoFrame } from "@/components/DemoFrame/DemoFrame"; -A common issue I see in frontend codebases, is we have have a component with an interface like this: + +Let's say we have a nice simple component like this: + + + { + await new Promise((res) => setTimeout(res, 1000)) + return {success: true} + }} state="pending" /> + + + +You click the button, it transitions to a loading state, then it transitions to either a success or error state. + +What I commonly see, is the component will be implemented with a interface like this: Whereby every bit of state that component could be in, is controlled by the parent and passed in as props. -Note that the decision to use an autocomplete component as the example is an intentional one - autocompletes are non-trivial - they can have issues with debouncing the live search, and out-of-order requests. +## Interfaces like this are a mistake There's a sense that components with an interface like this would be easy to test, we could write something like: @@ -21,47 +35,35 @@ It might be desirable to to write examples like this for Storybook, where we _ju But these tests do not test one of the key aspects of what we are interested in here - the state transitions. -We could start doing tests like this: - - +If we did want to test the state transitions, we could do it one of two ways: -but this really is not helpful as a test. +### 1. Create a wrapper component to contain the state in -Here in our test we're just asserting that the parent component correctly transition the prop changes on `isLoading` and `availableOptions` - we can't be sure that they're going to get that right. +Here, we are getting a good demonstration of 'tests are test of how usuable your code is' - that we're having to create a bit of external state and manage its transitions in the test, is _also_ what every consumer of our component is going to have to do. -## Components like this are a pain +### 2. Change the props via `rerender` -Components like this are tedious to use. -The problem with components like these is that the actual important functionality of the component has been left to the consumer. +This is no test at all. We're simply stipulating that the props changed accurately, there's no gurantee that our parent component will infact make this change correctly. -In order to use one of these components, we would have to write some code like this: +What this kind of interface really does, is it makes the _parent_ responsible for the logic of the state transition from pending to loading to error/success. - -🤮 +## The better interface - one that encapsulates the state transitions internally. -Imagine we had a form that had three such inputs! -## Instead, encapusulate as much state as you can in your component. -The better interface is one that looks like this: +Now we can write our tests like this: - -This allows the component to do a lot more the heavy lifting for you - "Hey, you give the async function, and I'll take care of all of the internal state for you". +This is much nicer! -Some example tests would look like this: - - - -These tests tell a story, they're a lot more useful as a test. ## There's nothing say that you couldn't do both -Technically what we could do is do something like expose these components with less functionality as `AutocompleteStateless` and then use this component in our `Autocomplete` component that then provides the the functionality. +Technically what we could do is do something like expose these components with less functionality as `SpecialButtonStateless` and then use this component in our `SpecialButton` component that then provides the the functionality. -I'm not sure how much value this would actually have, and I argue that it's the functioning `Autocomplete` component that is the important one. +I'm not sure how much value this would actually have, and I argue that it's the functioning `SpecialButton` component that is the important one. ## Why is this pattern so common? @@ -85,7 +87,13 @@ RTKQ does helpfully provide a [useLazyQuery](https://redux-toolkit.js.org/rtk-qu You can see [this discussion on TanStack's Github](https://github.com/TanStack/query/discussions/1205) the basic answer to why a lazy/imperative function is not provided, is because it would be trivial for someone to implement themselves. -This isn't at all to say that tools like Tanstack are bad, it's just that there is a case to be made for using async functions. Async function quite handily encapsulate the state of a query, as well as the data, all in one handy object. Tools like Tanstack still have their place in that they provide query duplication/caching. +Here is how we extract such a lazy query from TanStack - it really is not well advertised: + + +## This isn't an argument against state management tools like TanStack Query + +This isn't at all to say that tools like Tanstack are bad. Tools like Tanstack are very useful in that they provide query deduplication and caching. +But async interfaces, in my opinion\ From e817a86e7e82da6049388c0504f06301190ce352 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Sun, 19 Oct 2025 16:06:43 +1100 Subject: [PATCH 04/22] Update post --- src/demos/encapsulate-state/Demo.tsx | 9 +++++ .../encapsulate_as_much_state_as_possible.mdx | 36 +++++++++++++------ 2 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 src/demos/encapsulate-state/Demo.tsx diff --git a/src/demos/encapsulate-state/Demo.tsx b/src/demos/encapsulate-state/Demo.tsx new file mode 100644 index 00000000..e0ea2f20 --- /dev/null +++ b/src/demos/encapsulate-state/Demo.tsx @@ -0,0 +1,9 @@ +"use client"; +import { SpecialButton2 } from "./SpecialButton2"; + +export function EncapsulateStateDemo() { + return { + await new Promise((res) => setTimeout(res, 1000)); + return { success: true }; + }} /> +} \ No newline at end of file diff --git a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx index ea5aab45..1c055ad8 100644 --- a/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/drafts/encapsulate_as_much_state_as_possible.mdx @@ -5,17 +5,14 @@ meta: dateCreated: 2025-09-08 --- import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; -import { SpecialButton2 } from "@/demos/encapsulate-state/SpecialButton2"; +import { EncapsulateStateDemo } from "@/demos/encapsulate-state/Demo"; import { DemoFrame } from "@/components/DemoFrame/DemoFrame"; Let's say we have a nice simple component like this: - - { - await new Promise((res) => setTimeout(res, 1000)) - return {success: true} - }} state="pending" /> +Click the button to see it transition from pending to loading to success.

}> +
@@ -23,7 +20,7 @@ You click the button, it transitions to a loading state, then it transitions to What I commonly see, is the component will be implemented with a interface like this: - + Whereby every bit of state that component could be in, is controlled by the parent and passed in as props. @@ -31,6 +28,8 @@ Whereby every bit of state that component could be in, is controlled by the pare There's a sense that components with an interface like this would be easy to test, we could write something like: + + It might be desirable to to write examples like this for Storybook, where we _just_ want to see the component as it is loading. But these tests do not test one of the key aspects of what we are interested in here - the state transitions. @@ -39,31 +38,41 @@ If we did want to test the state transitions, we could do it one of two ways: ### 1. Create a wrapper component to contain the state in -Here, we are getting a good demonstration of 'tests are test of how usuable your code is' - that we're having to create a bit of external state and manage its transitions in the test, is _also_ what every consumer of our component is going to have to do. + + +Here, we are getting a good demonstration of 'tests are test of how usuable your code is' - that we're having to create a bit of external state and manage its transitions in the test, is _also what every consumer of our component is going to have to do_. ### 2. Change the props via `rerender` + This is no test at all. We're simply stipulating that the props changed accurately, there's no gurantee that our parent component will infact make this change correctly. -What this kind of interface really does, is it makes the _parent_ responsible for the logic of the state transition from pending to loading to error/success. +What this kind of interface really does, is it _makes the parent responsible for the logic of the state transition_ from pending to loading to error/success. + +Remember, the above example is a _very simple_ component. Imagine something like an Autocomplete component, which might contain state like what text the user has entered, is it loading, are there results, and so forth. ## The better interface - one that encapsulates the state transitions internally. +If instead we create an interface like this: + Now we can write our tests like this: + This is much nicer! +Our test now resembles how we're going to use the component in practise, and it's very straight forward what's happening. ## There's nothing say that you couldn't do both Technically what we could do is do something like expose these components with less functionality as `SpecialButtonStateless` and then use this component in our `SpecialButton` component that then provides the the functionality. -I'm not sure how much value this would actually have, and I argue that it's the functioning `SpecialButton` component that is the important one. +I think this would be of limited use - but might be helpful in a larger team with a dedicated design system, and wanting to see the component state statically. +I would argue that the functioning `SpecialButton` component that is the important for actually building the application. ## Why is this pattern so common? @@ -89,11 +98,16 @@ You can see [this discussion on TanStack's Github](https://github.com/TanStack/q Here is how we extract such a lazy query from TanStack - it really is not well advertised: + + +We use TanStack Query's `queryClient.ensureQueryData` + + ## This isn't an argument against state management tools like TanStack Query This isn't at all to say that tools like Tanstack are bad. Tools like Tanstack are very useful in that they provide query deduplication and caching. -But async interfaces, in my opinion\ +However, async functions as a particular example, are often the right abstraction to represent 'user does a thing, it loads for a bit, and then the state changes'. From a60e8045446813b9462dba173afe2934c71cd3d7 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Mon, 20 Oct 2025 16:48:34 +1100 Subject: [PATCH 05/22] Update example. --- src/demos/encapsulate-state/SpecialButton.tsx | 3 --- .../encapsulate_as_much_state_as_possible.mdx | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/demos/encapsulate-state/SpecialButton.tsx b/src/demos/encapsulate-state/SpecialButton.tsx index 7f2efc11..e15cf49a 100644 --- a/src/demos/encapsulate-state/SpecialButton.tsx +++ b/src/demos/encapsulate-state/SpecialButton.tsx @@ -8,9 +8,6 @@ type SpecialButtonProps = { }; export function SpecialButton(props: SpecialButtonProps) { - - - return -} \ No newline at end of file diff --git a/src/demos/encapsulate-state/SpecialButton2.tsx b/src/demos/encapsulate-state/SpecialButton2.tsx deleted file mode 100644 index 98d1f5a9..00000000 --- a/src/demos/encapsulate-state/SpecialButton2.tsx +++ /dev/null @@ -1,30 +0,0 @@ - -"use client" - -import './special-button.css'; -import { useState } from 'react'; - -type SpecialButtonProps = { - onClick: () => Promise<{ success: boolean }>; -}; - -export function SpecialButton2(props: SpecialButtonProps) { - const [state, setState] = useState<"loading" | "error" | "success" | "pending">("pending"); - - const handleClick = async () => { - setState("loading"); - try { - const result = await props.onClick(); - setState(result.success ? "success" : "error"); - } catch (error) { - setState("error"); - } - }; - - return -} \ No newline at end of file diff --git a/src/demos/encapsulate-state/SpecialButtonDemo.tsx b/src/demos/encapsulate-state/SpecialButtonDemo.tsx index d57ee66b..e04945dd 100644 --- a/src/demos/encapsulate-state/SpecialButtonDemo.tsx +++ b/src/demos/encapsulate-state/SpecialButtonDemo.tsx @@ -1,5 +1,5 @@ "use client"; -import { SpecialButton2 } from "./SpecialButton2"; +import { SpecialButton2 } from "@blacksheepcode/example-react-autocomplete"; export function SpecialButtonDemo() { return { diff --git a/src/demos/encapsulate-state/special-button.css b/src/demos/encapsulate-state/special-button.css deleted file mode 100644 index e87485b5..00000000 --- a/src/demos/encapsulate-state/special-button.css +++ /dev/null @@ -1,29 +0,0 @@ -.special-button { - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.special-button.pending { - background-color: #007bff; - color: white; -} - -.special-button.loading { - background-color: #6c757d; - color: white; - cursor: not-allowed; -} - -.special-button.success { - background-color: #28a745; - color: white; -} - -.special-button.error { - background-color: #dc3545; - color: white; -} \ No newline at end of file From 7bb98744ece52b94f9e53aa2cbacc90c273406b4 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Thu, 23 Oct 2025 14:22:17 +1100 Subject: [PATCH 13/22] Css fixes. --- src/app/globals.css | 4 ++++ src/components/BlogPostFrame/react-text-highlight.css | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/globals.css b/src/app/globals.css index b1d34dd9..248bbcb7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -57,6 +57,10 @@ body { --column-width: 1100px; } +img { + max-width: 100%; +} + pre, pre:hover { white-space: pre-wrap; diff --git a/src/components/BlogPostFrame/react-text-highlight.css b/src/components/BlogPostFrame/react-text-highlight.css index efa3d316..d1c7e724 100644 --- a/src/components/BlogPostFrame/react-text-highlight.css +++ b/src/components/BlogPostFrame/react-text-highlight.css @@ -56,7 +56,7 @@ @media (prefers-color-scheme: dark) { :root { - --highlight-bg-color: #4a4b80; + --highlight-bg-color: #5790d459; --highlight-text-color: #fff; --highlight-bg-color-hover: #4a4b80; --highlight-border-hover: #5b5f99; From 1cedcf5820d8da01221e5936eb31cc8e2ba48c50 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Thu, 23 Oct 2025 14:24:44 +1100 Subject: [PATCH 14/22] Typos --- src/routes/posts/encapsulate_as_much_state_as_possible.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx index 9f9f286c..4579ef40 100644 --- a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx @@ -55,7 +55,7 @@ Here, we are getting a good demonstration of 'tests are test of how usable your -This is no test at all. We're simply stipulating that the props changed accurately, there's no guarantee that our parent component will infact make this change correctly. +This is no test at all. We're simply stipulating that the props changed accurately, there's no guarantee that our parent component will in fact make this change correctly. What this kind of interface really does, is it _makes the parent responsible for the logic of the state transition_ from pending to loading to error/success. @@ -170,4 +170,4 @@ The use of it is _much_ simpler: Importantly, coming back to the debouncing, Cancelling logic will require changing the component interface, such that the searchFn returns some kind of AbortablePromise instead of a regular Promise. -

}>cancelling
, pagination logic, all of that is _encapusulated_, hidden away inside, the component - the consumer does not need to think about, it's all taken care of for them. \ No newline at end of file +

}>cancelling, pagination logic, all of that is _encapsulated_, hidden away inside, the component - the consumer does not need to think about, it's all taken care of for them. \ No newline at end of file From d2b59183a748efde54859fb6866dc835a4d35692 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Thu, 23 Oct 2025 15:01:43 +1100 Subject: [PATCH 15/22] Try +https --- next-env.d.ts | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/next-env.d.ts b/next-env.d.ts index 1b3be084..830fb594 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 55ec445a..1a46b97f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@blacksheepcode/react-text-highlight": "^0.4.0", - "@blacksheepcode/example-react-autocomplete": "git://github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", + "@blacksheepcode/example-react-autocomplete": "git+https://github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", "@mdx-js/loader": "^3.0.1", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", From d375530894a51dc29ce897219267b6284c95285c Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 14:53:26 +1100 Subject: [PATCH 16/22] empty commit From 9a274fcff589119343634b2ec4931807cb81c016 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 15:41:44 +1100 Subject: [PATCH 17/22] Update post --- package-lock.json | 12 ++++++------ package.json | 2 +- .../encapsulate-state/AutocompleteDemo.tsx | 5 +++-- .../encapsulate_as_much_state_as_possible.mdx | 19 +++++++++++++++---- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40d4eea0..1611a389 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@blacksheepcode/example-react-autocomplete": "git://github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", + "@blacksheepcode/example-react-autocomplete": "git+https://github.com/dwjohnston/example-react-autocomplete.git#c8e74acf9d10aef0dfc2b6f6d33ee811371ab99a", "@blacksheepcode/react-text-highlight": "^0.4.0", "@mdx-js/loader": "^3.0.1", "@mdx-js/mdx": "^3.0.1", @@ -530,8 +530,8 @@ "node_modules/@blacksheepcode/example-react-autocomplete": { "name": "example-react-autocomplete", "version": "0.1.0", - "resolved": "git+ssh://git@github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", - "integrity": "sha512-urMJsynGdEx/ZJ9iKcfIYjOVPefn7Xq6iu9XpYxmp/z+q+9dYWWakqEDChLameS9MIfHffJpF88ZkTw2W8GiXw==", + "resolved": "git+ssh://git@github.com/dwjohnston/example-react-autocomplete.git#c8e74acf9d10aef0dfc2b6f6d33ee811371ab99a", + "integrity": "sha512-614u8N7AHeN2pcnIb1TcmNjvL/rzTAVEQVOuVqUJGL/l12LYuorRJI3G6N4d+M51qACmSWc76+SiJy2mpqBkfw==", "dependencies": { "@tanstack/react-query": "^5.87.1" }, @@ -18291,9 +18291,9 @@ } }, "@blacksheepcode/example-react-autocomplete": { - "version": "git+ssh://git@github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", - "integrity": "sha512-urMJsynGdEx/ZJ9iKcfIYjOVPefn7Xq6iu9XpYxmp/z+q+9dYWWakqEDChLameS9MIfHffJpF88ZkTw2W8GiXw==", - "from": "@blacksheepcode/example-react-autocomplete@git://github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", + "version": "git+ssh://git@github.com/dwjohnston/example-react-autocomplete.git#c8e74acf9d10aef0dfc2b6f6d33ee811371ab99a", + "integrity": "sha512-614u8N7AHeN2pcnIb1TcmNjvL/rzTAVEQVOuVqUJGL/l12LYuorRJI3G6N4d+M51qACmSWc76+SiJy2mpqBkfw==", + "from": "@blacksheepcode/example-react-autocomplete@git+https://github.com/dwjohnston/example-react-autocomplete.git#c8e74acf9d10aef0dfc2b6f6d33ee811371ab99a", "requires": { "@tanstack/react-query": "^5.87.1" } diff --git a/package.json b/package.json index 1a46b97f..56532e20 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@blacksheepcode/react-text-highlight": "^0.4.0", - "@blacksheepcode/example-react-autocomplete": "git+https://github.com/dwjohnston/example-react-autocomplete.git#8ad175616e705a74665f0cf70800f82d90c4bc4e", + "@blacksheepcode/example-react-autocomplete": "git+https://github.com/dwjohnston/example-react-autocomplete.git#c8e74acf9d10aef0dfc2b6f6d33ee811371ab99a", "@mdx-js/loader": "^3.0.1", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", diff --git a/src/demos/encapsulate-state/AutocompleteDemo.tsx b/src/demos/encapsulate-state/AutocompleteDemo.tsx index 4379a2d6..3ac22d14 100644 --- a/src/demos/encapsulate-state/AutocompleteDemo.tsx +++ b/src/demos/encapsulate-state/AutocompleteDemo.tsx @@ -64,7 +64,7 @@ const searchFn = async (searchTerm: string, pageNumber: number) => { } const filteredItems = mockItems.filter(item => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) + (`item.name ${item.description}`).toLowerCase().includes(searchTerm.toLowerCase()) ); return { @@ -81,8 +81,9 @@ export function AutocompleteDemo() {
{item.name}
} + renderItem={(item) =>
{item.name} - {item.description}
} itemKey="id" + selectedValueDisplayStringFn={(item) => item.name} />
diff --git a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx index 4579ef40..fdb7bcdb 100644 --- a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx @@ -161,13 +161,24 @@ Chances are, the developer will be See + The use of it is _much_ simpler: - + + +And writing tests make sense: + + Importantly, coming back to the debouncing, Cancelling logic will require changing the component interface, such that the searchFn returns some kind of AbortablePromise instead of a regular Promise. -

}>cancelling
, pagination logic, all of that is _encapsulated_, hidden away inside, the component - the consumer does not need to think about, it's all taken care of for them. \ No newline at end of file +

}>cancelling
, pagination logic, all of that is _encapsulated_, hidden away inside, the component - the consumer does not need to think about, it's all taken care of for them. + +Now you might think that that + Not to mention that this implementation doesn't allow for an initially selected value - we would need to add an `initialValue` and `getPrettyNameForInitialValue` props. +

}>`selectedValueDisplayStringFn` stuff
doesn't really look like we're _simplifying_ the interface - but we are. + +The component interface is _forcing_ the developer handle the transition from 'having a search term entered and selecting an item' to 'an item is selected and something is displayed in the search box', and it does this does this in a manner that can't forgotten (you'll get a type error). + +The complexity of the interface comes from the inherrent complexity of the problem space - and as much as possible, the component has already done the thinking for you as a developer - and it's telling you 'these are the decision you need to make - what should be displayed when an item is selected?'. \ No newline at end of file From fc4b4cd2271ef19539aa0c59322e3be9bb7dc3c9 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 15:42:07 +1100 Subject: [PATCH 18/22] Update date. --- src/routes/posts/encapsulate_as_much_state_as_possible.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx index fdb7bcdb..e0d92222 100644 --- a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx @@ -2,7 +2,7 @@ meta: title: Encapsulate as much state as possible in your component description: A common pattern I see is passing every bit of a component's state in as a prop. This just puts the responsibility of that component's actual useful logic on to the parent. - dateCreated: 2025-10-23 + dateCreated: 2025-10-31 --- import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; import { SpecialButtonDemo } from "@/demos/encapsulate-state/SpecialButtonDemo"; From 30e60bf0d773f2409dfb96aaa4458e48905b6f65 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 15:45:55 +1100 Subject: [PATCH 19/22] Add tags, screenshot --- src/assets/autocomplete.png | Bin 0 -> 36702 bytes .../encapsulate_as_much_state_as_possible.mdx | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 src/assets/autocomplete.png diff --git a/src/assets/autocomplete.png b/src/assets/autocomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..8123ddfee5ea94d4c836f35ba76deb76bd62c010 GIT binary patch literal 36702 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
`1mx^+^>64!bSlfxaT}O1MPCMbph5K1C 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 ZSFmNhnr(d?`Uw1!kx&r-CaU}Se*x30KFR<9 literal 0 HcmV?d00001 diff --git a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx index e0d92222..f6e7810b 100644 --- a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx @@ -3,6 +3,12 @@ meta: title: Encapsulate as much state as possible in your component description: A common pattern I see is passing every bit of a component's state in as a prop. This just puts the responsibility of that component's actual useful logic on to the parent. dateCreated: 2025-10-31 + image: "autocomplete" + +tags: + - "react" + - "testing" + --- import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; import { SpecialButtonDemo } from "@/demos/encapsulate-state/SpecialButtonDemo"; From af1bd9e47bb8a719f0e4edb92cf07499d238458c Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 15:56:31 +1100 Subject: [PATCH 20/22] Update src/demos/encapsulate-state/AutocompleteDemo.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/demos/encapsulate-state/AutocompleteDemo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/demos/encapsulate-state/AutocompleteDemo.tsx b/src/demos/encapsulate-state/AutocompleteDemo.tsx index 3ac22d14..a8f58063 100644 --- a/src/demos/encapsulate-state/AutocompleteDemo.tsx +++ b/src/demos/encapsulate-state/AutocompleteDemo.tsx @@ -64,7 +64,7 @@ const searchFn = async (searchTerm: string, pageNumber: number) => { } const filteredItems = mockItems.filter(item => - (`item.name ${item.description}`).toLowerCase().includes(searchTerm.toLowerCase()) + (`${item.name} ${item.description}`).toLowerCase().includes(searchTerm.toLowerCase()) ); return { From eaf796d2a574bcd45b7e634a557c65a7882ba9ba Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 16:12:27 +1100 Subject: [PATCH 21/22] Improve comment. --- .../posts/encapsulate_as_much_state_as_possible.mdx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx index f6e7810b..ced42cf5 100644 --- a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx @@ -181,9 +181,16 @@ Importantly, coming back to the debouncing, Cancelling logic will require changing the component interface, such that the searchFn returns some kind of AbortablePromise instead of a regular Promise.

}>cancelling
, pagination logic, all of that is _encapsulated_, hidden away inside, the component - the consumer does not need to think about, it's all taken care of for them. -Now you might think that that - Not to mention that this implementation doesn't allow for an initially selected value - we would need to add an `initialValue` and `getPrettyNameForInitialValue` props. -

}>`selectedValueDisplayStringFn` stuff
doesn't really look like we're _simplifying_ the interface - but we are. +Now you might think that that

+ Not to mention that this implementation doesn't allow for an initially selected value - we would need to add an `initialValue` and an async function `fetchValueById` props. +

+

+ The thinking here is that we shouldn't need a full `T` object to provide as the initial value. Often when a page or form loads - we just have the ID of the thing (a userId, a todoId, etc). +

+

+ Rather than the parent component loading the thing, and _then_ showing the autocomplete, the parent component simply provides an async function in order for the Autocomplete to fetch it itself. +

+}>`selectedValueDisplayStringFn` stuff
doesn't really look like we're _simplifying_ the interface - but we are. The component interface is _forcing_ the developer handle the transition from 'having a search term entered and selecting an item' to 'an item is selected and something is displayed in the search box', and it does this does this in a manner that can't forgotten (you'll get a type error). From 51e26acd6b09b57907c38497b676c19097f11851 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 31 Oct 2025 16:23:44 +1100 Subject: [PATCH 22/22] Ugh. bs MDX problem. --- .../posts/encapsulate_as_much_state_as_possible.mdx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx index ced42cf5..d10da11b 100644 --- a/src/routes/posts/encapsulate_as_much_state_as_possible.mdx +++ b/src/routes/posts/encapsulate_as_much_state_as_possible.mdx @@ -182,14 +182,8 @@ Importantly, coming back to the debouncing,

}>cancelling
, pagination logic, all of that is _encapsulated_, hidden away inside, the component - the consumer does not need to think about, it's all taken care of for them. Now you might think that that

- Not to mention that this implementation doesn't allow for an initially selected value - we would need to add an `initialValue` and an async function `fetchValueById` props. -

-

- The thinking here is that we shouldn't need a full `T` object to provide as the initial value. Often when a page or form loads - we just have the ID of the thing (a userId, a todoId, etc). -

-

- Rather than the parent component loading the thing, and _then_ showing the autocomplete, the parent component simply provides an async function in order for the Autocomplete to fetch it itself. -

+ Not to mention that this implementation doesn't allow for an initially selected value - we would need to add an `initialValue` and an async function `fetchValueById` props.

The thinking here is that we shouldn't need a full `T` object to provide as the initial value. Often when a page or form loads - we just have the ID of the thing (a userId, a todoId, etc).

+ Rather than the parent component loading the thing, and _then_ showing the autocomplete, the parent component simply provides an async function in order for the Autocomplete to fetch it itself.

}>`selectedValueDisplayStringFn` stuff
doesn't really look like we're _simplifying_ the interface - but we are. The component interface is _forcing_ the developer handle the transition from 'having a search term entered and selecting an item' to 'an item is selected and something is displayed in the search box', and it does this does this in a manner that can't forgotten (you'll get a type error).