Skip to content

Commit 708dc4e

Browse files
authored
Merge pull request #688 from fable-hub/v3/rework_memo
V3/rework memo
2 parents 92fca4c + b6775e6 commit 708dc4e

45 files changed

Lines changed: 1961 additions & 1127 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ root.render (Counter())
3737
### ✨ Features
3838

3939
- Flexible **API design**: Combine the reliability of F# type safety with the flexibility to interop easily with native JavaScript.
40-
- Discoverable **attributes** with no more functions, `Html` attributes or css properties globally available so they are easy to find.
40+
- Discoverable **attributes** with no more functions, `Html` attributes or CSS properties globally available so they are easy to find.
4141
- Proper **documentation**: each attribute and CSS property
4242
- Full **React API** support: Feliz aims to support the React API for building components using hooks, context and more.
4343
- Fully **Type-safe**: no more `Margin of obj` but instead utilizing a plethora of overloaded functions to account for the overloaded nature of `CSS` attributes, covering 90%+ of the CSS styles, values and properties.
4444
- **Compatible** with [Femto](https://github.com/Zaid-Ajaj/Femto).
45-
- Approximately **Zero** bundle size increase where everything function body is erased from the generated javascript unless you actually use said function.
45+
- Approximately **Zero** bundle size increase where everything function body is erased from the generated JavaScript unless you actually use said function.
4646

4747
### 🚀 Quick Start
4848

@@ -63,4 +63,4 @@ npm start
6363

6464
### 📚 Documentation
6565

66-
Feliz has extensive documentation at [https://zaid-ajaj.github.io/Feliz](https://zaid-ajaj.github.io/Feliz) with live examples along side code samples, check them out and if you have any question, let us know!
66+
Feliz has extensive documentation at [https://zaid-ajaj.github.io/Feliz](https://zaid-ajaj.github.io/Feliz) with live examples alongside code samples, check them out and if you have any question, let us know!

docs/docs/api-docs/feliz/react-component.mdx

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -192,40 +192,105 @@ import ReactComponentImport from '../../feliz-docs/fableoutput/Examples/Feliz/Re
192192

193193
React `memo` components can be created using the `[<ReactMemoComponent>]` attribute. It works the same way as `[<ReactComponent>]`, but wraps the component in `React.memo` and ensures that it is defined as a `const` in the generated JavaScript code.
194194

195-
<Tabs>
195+
import MemoAttribute from '../../feliz-docs/fableoutput/Examples/React/MemoAttribute'
196+
import RawMemoAttribute from '!!raw-loader!../../feliz-docs/Examples/React/MemoAttribute.fs'
196197

197-
<TabItem value="F#" label="F#">
198+
<ComponentRender code={RawMemoAttribute}>
199+
<MemoAttribute />
200+
</ComponentRender>
201+
202+
### areEqual with js emit
203+
204+
You can pass a custom equality function to the `[<ReactMemoComponent>]` attribute using the `areEqual` parameter. The value must be a JavaScript function expressed as a string. This function will be used to determine whether the component should re-render based on its props.
198205

199206
```fsharp
200-
[<ReactMemoComponent>]
201-
let Component(text: string, count: int) =
202-
Html.div [
203-
for i in 1 .. count do
204-
Html.p [
205-
prop.key i
206-
prop.text (sprintf "%d: %s" i text)
207-
]
208-
]
207+
[<ReactMemoComponent("(prevProps, nextProps) =>
208+
prevProps.fruits.length === nextProps.fruits.length
209+
&& prevProps.fruits.every((value, index) =>
210+
value === nextProps.fruits[index]
211+
)"
212+
)>]
213+
let RenderTextWithEffect (fruits: string []) =
214+
```
215+
216+
This behavior is similiar to the `[<Emit>]` attribute in Fable.
217+
218+
:::info
219+
`areEqual` is implemented using the `[<StringSyntax("javascript")>]` attribute. This will provide syntax highlighting and basic validation in supported editors.
220+
221+
<details>
222+
<summary>Example: Rider</summary>
223+
224+
225+
import RiderStringSyntax from '../../../static/img/rider-stringsyntax-attribute.png'
226+
227+
<img src={RiderStringSyntax} alt="Rider StringSyntax Attribute" />;
228+
229+
230+
</details>
231+
232+
:::
233+
234+
:::danger
235+
The areEqual functions runs on the transformed js output, so you must assume, that your params are passed as a single object containing all props!
236+
:::
237+
238+
import MemoAttributeAreEqualEmit from '../../feliz-docs/fableoutput/Examples/React/MemoAttributeAreEqualEmit'
239+
import RawMemoAttributeAreEqualEmit from '!!raw-loader!../../feliz-docs/Examples/React/MemoAttributeAreEqualEmit.fs'
240+
241+
<ComponentRender code={RawMemoAttributeAreEqualEmit}>
242+
<MemoAttributeAreEqualEmit />
243+
</ComponentRender>
244+
245+
### areEqual with F# function
246+
247+
We can also emit a call to a f# function defined in the same file. This function must have the correct signature to be used as an equality function.
209248

249+
```fsharp
250+
let areEqualFn prop1 prop2 =
251+
prop1 = prop2
252+
253+
[<ReactMemoComponent(nameof areEqualFn)>] // or "areEqualFn"
254+
let RenderTextWithEffect (fruits: string []) =
255+
React.useEffect (fun () -> console.log("Rerender!") )
256+
Html.div [
257+
prop.text (fruits |> String.concat ", ");
258+
prop.testId "memo-attribute"
259+
]
210260
```
211261

212-
</TabItem>
262+
:::info
263+
This works as f# equality does not check reference equality for arrays and sequences, but checks the content of them. Which is what is used by shallow comparison of React.memo.
264+
:::
213265

214-
<TabItem value="JSX" label="JSX">
266+
:::danger
267+
The areEqual functions runs on the transformed js output, so you must assume, that your params are passed as a single object containing all props!
268+
269+
In the example above, if you want to compare the `fruits` prop, you need to access it as `prop1.fruits` and `prop2.fruits` in the `areEqualFn` function:
215270

216-
```jsx
217-
export const Component = memo((componentInputProps) => {
218-
const count = componentInputProps.count;
219-
const text = componentInputProps.text;
220-
// ...
221-
});
271+
```fsharp
272+
let areEqualFn (prop1: {|fruits: string []|}) (prop2: {|fruits: string []|}) =
273+
prop1.fruits = prop2.fruits
222274
```
275+
:::
223276

224-
</TabItem>
277+
:::danger[Name Mangling]
278+
If you define your equality function as static member or inside a module, Fable might mangle the name of the function during transpilation. In this case, you need to provide the mangled name to the `areEqual` parameter. You can see this behavior int his [Fable Repl](https://fable.io/repl/#?code=PYBwpgdgBAYghgIwDZgHQGFgCcwChcC2wAJgK4pQCiAjqXEgJYAuAnjNALy5Q9QpNQ4OGnSTsoILKACMEqSABMULr1VyZy9Yvz8oAD00Q4BMMABmVWvWZsIqIWBH1xAehdQAzgAtg5YlAQwAC5LURt2AH0HJzEIfFZwUOtWdnQkOA8PWRVeDyY4JgYAYygTAkCsQWErWK1ZSVAlHLU6zQbtXF0WQ2NTCxjwiDSMrPtq0Vd3AZSh9MzpKPHnCCA&html=Q&css=Q).
225279

280+
```fsharp
281+
module EqualityFn =
282+
let areEqualFn prop1 prop2 =
283+
prop1 = prop2
284+
```
285+
:::
226286

227-
</Tabs>
287+
import MemoAttributeAreEqualFnEmit from '../../feliz-docs/fableoutput/Examples/React/MemoAttributeAreEqualEmitFnName'
288+
import RawMemoAttributeAreEqualFnEmit from '!!raw-loader!../../feliz-docs/Examples/React/MemoAttributeAreEqualEmitFnName.fs'
289+
import RawMemoAttributeAreEqualFnEmitJS from '!!raw-loader!../../feliz-docs/fableoutput/Examples/React/MemoAttributeAreEqualEmitFnName.jsx'
228290

291+
<ComponentRender code={[{language: 'fsharp', fileName: 'MemoAttributeAreEqualEmitFnName.fs', content: RawMemoAttributeAreEqualFnEmit}, {language: 'jsx', fileName: 'MemoAttributeAreEqualEmitFnName.jsx', content: RawMemoAttributeAreEqualFnEmitJS}]}>
292+
<MemoAttributeAreEqualFnEmit />
293+
</ComponentRender>
229294

230295
## `[<ReactLazyComponent>]`
231296

docs/docs/api-docs/guides/fable.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import TabItem from '@theme/TabItem';
1313

1414
The following is a collection of short guides and tips for common pitfalls when using Fable for frontend development. Feel free to checkout the categories on the in-page table of contents in the sidebar and navigate to the topic you are interested in.
1515

16+
:::info[Fable]
17+
More on Fable can be found in the ecosystem section [here](../../ecosystem/01_Tools/Fable.mdx).
18+
:::
19+
1620
:::tip[Fable official docs]
1721

1822
Altough I might repeat some of the content here, I highly recommend you to check out the [official Fable JavaScript documentation](https://fable.io/docs/javascript/features.html). It contains a lot of useful information.

docs/docs/api-docs/react/apis/memo.mdx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,58 @@ import ReactRefAdmonition from '@site/src/components/ReactRefAdmonition';
1111

1212
`React.memo` lets you optimize components by memoizing their output. This prevents unnecessary rerenders when the props haven't changed, improving performance for pure components.
1313

14+
Without memoization, a component will rerender whenever its parent rerenders, even if its props remain the same. By wrapping a component with `React.memo`, React will skip rendering the component and reuse the last rendered output if the props are unchanged.
15+
16+
## ReactMemoComponentAttribute
17+
18+
The `[<ReactMemoComponent>]` attribute provides a convenient way to memoize functional components in Feliz. It does the transformation on transpile level so you don't have to wrap your component manually.
19+
1420
```fsharp
1521
[<ReactMemoComponent>] // memoizes component to prevent rerender whenever parent rerenders
1622
let ChildComponent (onClick: unit -> unit) =
1723
// ... component implementation
1824
```
1925

20-
import UseCallback from '../../../feliz-docs/fableoutput/Examples/React/UseCallback'
21-
import RawUseCallback from '!!raw-loader!../../../feliz-docs/Examples/React/UseCallback.fs'
26+
Check out the [ReactMemoComponentAttribute documentation](../../feliz/react-component.mdx) for more details.
27+
28+
import MemoAttribute from '../../../feliz-docs/fableoutput/Examples/React/MemoAttribute'
29+
import RawMemoAttribute from '!!raw-loader!../../../feliz-docs/Examples/React/MemoAttribute.fs'
30+
31+
<ComponentRender code={RawMemoAttribute}>
32+
<MemoAttribute />
33+
</ComponentRender>
34+
35+
## React.memo function
36+
37+
:::warning
38+
This approach has several limitations compared to using the `[<ReactMemoComponent>]` attribute (see example below):
39+
- You must use a `let` binding for your component, to ensure Fable transpiling as `const`.
40+
- You must use any F# type transpiling to a JavaScript object for props (e.g., anonymous record, `[<PojoAttribute>]`).
41+
:::
42+
43+
```fsharp
44+
let MemoizedComponent =
45+
React.memo<{|text: string|}> (fun props ->
46+
// ... component implementation
47+
)
48+
```
49+
50+
import MemoFunction from '../../../feliz-docs/fableoutput/Examples/React/MemoFunction'
51+
import RawMemoFunction from '!!raw-loader!../../../feliz-docs/Examples/React/MemoFunction.fs'
52+
53+
<ComponentRender code={RawMemoFunction}>
54+
<MemoFunction />
55+
</ComponentRender>
56+
57+
## areEqual
58+
59+
The `React.memo` function also accepts an optional second argument, `areEqual`, which is a custom comparison function for props. This function receives the previous and next props and should return `true` if they are equal (i.e., no rerender needed) or `false` if they are different (i.e., rerender needed).
60+
61+
This is useful as memo only does a shallow comparison of props by default. If your props are complex objects or arrays, you may need to provide a custom comparison function to accurately determine equality.
62+
63+
import MemoFunctionAreEqual from '../../../feliz-docs/fableoutput/Examples/React/MemoFunctionAreEqual'
64+
import RawMemoFunctionAreEqual from '!!raw-loader!../../../feliz-docs/Examples/React/MemoFunctionAreEqual.fs'
2265

23-
<ComponentRender code={RawUseCallback}>
24-
<UseCallback />
66+
<ComponentRender code={RawMemoFunctionAreEqual}>
67+
<MemoFunctionAreEqual />
2568
</ComponentRender>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module Example.MemoAttribute
2+
3+
open Feliz
4+
open Browser.Dom
5+
6+
[<ReactMemoComponent>]
7+
let RenderTextWithEffect (text: string) =
8+
React.useEffect (fun () -> console.log("Rerender!", text) )
9+
Html.div [
10+
prop.text text;
11+
prop.testId "memo-attribute"
12+
]
13+
14+
15+
[<ReactComponent(true)>]
16+
let Main () =
17+
let isDark, setIsDark = React.useState(false)
18+
let text, setText = React.useState("Hello, world!")
19+
let fgColor = if isDark then color.white else color.black
20+
let bgColor = if isDark then color.black else color.white
21+
Html.div [
22+
prop.style [style.border(1, borderStyle.solid, fgColor); style.padding 20; style.color fgColor; style.backgroundColor bgColor]
23+
prop.children [
24+
Html.h3 "Check the output in the browser console"
25+
Html.p "The child component below is memoized using the [<ReactMemoComponent>] attribute. It only rerenders when its props change."
26+
Html.button [
27+
prop.text "Toggle Dark Mode"
28+
prop.onClick (fun _ -> setIsDark(not isDark))
29+
]
30+
Html.input [
31+
prop.value text
32+
prop.onChange setText
33+
]
34+
RenderTextWithEffect(text)
35+
]
36+
]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module Example.MemoAttributeAreEqualEmit
2+
3+
open Feliz
4+
open Browser.Dom
5+
6+
[<ReactMemoComponent("(prevProps, nextProps) =>
7+
prevProps.fruits.length === nextProps.fruits.length
8+
&& prevProps.fruits.every((value, index) =>
9+
value === nextProps.fruits[index]
10+
)"
11+
)>]
12+
let RenderTextWithEffect (fruits: string []) =
13+
React.useEffect (fun () -> console.log("Rerender!") )
14+
Html.div [
15+
prop.text (fruits |> String.concat ", ");
16+
prop.testId "memo-attribute"
17+
]
18+
19+
[<ReactComponent(true)>]
20+
let Main () =
21+
let isDark, setIsDark = React.useState(false)
22+
let input, setInput = React.useState("")
23+
// This will stay the same array if it does not change
24+
let fruits, setFruits = React.useState([|"apple"; "orange"; "banana"|])
25+
// This creates a new array reference on every render, triggering a rerender of the child component, without custom equality check
26+
let sortedFruits =
27+
fruits
28+
|> Array.sort
29+
let isValidInput = System.String.IsNullOrEmpty input |> not && fruits |> Array.contains input |> not
30+
let fgColor = if isDark then color.white else color.black
31+
let bgColor = if isDark then color.black else color.white
32+
Html.div [
33+
prop.style [style.border(1, borderStyle.solid, fgColor); style.padding 20; style.color fgColor; style.backgroundColor bgColor]
34+
prop.children [
35+
Html.h3 "Check the output in the browser console"
36+
Html.p "The child component below is memoized using the [<ReactMemoComponent>] attribute. It only rerenders when the areEqual function returns false."
37+
Html.button [
38+
prop.text "Toggle Dark Mode"
39+
prop.onClick (fun _ -> setIsDark(not isDark))
40+
]
41+
Html.input [
42+
prop.value input
43+
prop.onChange setInput
44+
]
45+
Html.button [
46+
prop.text "Change Fruits Array"
47+
prop.disabled (not isValidInput)
48+
prop.onClick (fun _ ->
49+
if isValidInput then
50+
[|yield! fruits; input|] |> setFruits
51+
setInput ""
52+
)
53+
]
54+
RenderTextWithEffect(sortedFruits)
55+
]
56+
]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
module Example.MemoAttributeAreEqualEmitFnName
2+
3+
open Feliz
4+
open Browser.Dom
5+
6+
let areEqualFn prop1 prop2 =
7+
prop1 = prop2
8+
9+
[<ReactMemoComponent(nameof areEqualFn)>] // or "areEqualFn"
10+
let RenderTextWithEffect (fruits: string []) =
11+
React.useEffect (fun () -> console.log("Rerender!") )
12+
Html.div [
13+
prop.text (fruits |> String.concat ", ");
14+
prop.testId "memo-attribute"
15+
]
16+
17+
[<ReactComponent(true)>]
18+
let Main () =
19+
let isDark, setIsDark = React.useState(false)
20+
let input, setInput = React.useState("")
21+
// This will stay the same array if it does not change
22+
let fruits, setFruits = React.useState([|"apple"; "orange"; "banana"|])
23+
// This creates a new array reference on every render, triggering a rerender of the child component, without custom equality check
24+
let sortedFruits =
25+
fruits
26+
|> Array.sort
27+
let isValidInput = System.String.IsNullOrEmpty input |> not && fruits |> Array.contains input |> not
28+
let fgColor = if isDark then color.white else color.black
29+
let bgColor = if isDark then color.black else color.white
30+
Html.div [
31+
prop.style [style.border(1, borderStyle.solid, fgColor); style.padding 20; style.color fgColor; style.backgroundColor bgColor]
32+
prop.children [
33+
Html.h3 "Check the output in the browser console"
34+
Html.p "The child component below is memoized using the [<ReactMemoComponent>] attribute. It only rerenders when the areEqual function returns false."
35+
Html.button [
36+
prop.text "Toggle Dark Mode"
37+
prop.onClick (fun _ -> setIsDark(not isDark))
38+
]
39+
Html.input [
40+
prop.value input
41+
prop.onChange setInput
42+
]
43+
Html.button [
44+
prop.text "Change Fruits Array"
45+
prop.disabled (not isValidInput)
46+
prop.onClick (fun _ ->
47+
if isValidInput then
48+
[|yield! fruits; input|] |> setFruits
49+
setInput ""
50+
)
51+
]
52+
RenderTextWithEffect(sortedFruits)
53+
]
54+
]

0 commit comments

Comments
 (0)