Skip to content

Commit fa98a99

Browse files
authored
Merge pull request #689 from fable-hub/v3/memo/predefined_equality
Add support for predefined equality functions for `[<ReactMemoComponnt>]`
2 parents 9226c9d + 71e3123 commit fa98a99

14 files changed

Lines changed: 443 additions & 75 deletions

File tree

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,24 @@ import RawMemoAttribute from '!!raw-loader!../../feliz-docs/Examples/React/MemoA
199199
<MemoAttribute />
200200
</ComponentRender>
201201

202-
### areEqual with js emit
202+
### areEqual - Feliz predefined functions
203+
204+
Feliz features prewritten equality functions for common scenarios that can be used with the `areEqualFn` parameter of the `[<ReactMemoComponent>]` attribute. This parameter
205+
accepts an integer value that corresponds to the desired equality function:
206+
207+
- `0`/`AreEqualFn.FsEquals` - Uses F#'s built-in equality comparison (`=`) for all props. So everything that normally returns true when using F# equality `=` will return true here as well.
208+
- `1`/`AreEqualFn.FsEqualsButFunctions` - Uses F#'s built-in equality comparison (`=`) for all props, but ignores all functions in props. This is useful when you have functions as props that are recreated on each render, but you want to ignore them for equality checks.
209+
210+
You can also use defined integer constants for example: `AreEqualFn.FsEquals` for better readability.
211+
212+
import MemoAttributeAreEqualFn from '../../feliz-docs/fableoutput/Examples/Feliz/ReactMemoComponentAreEqualFn'
213+
import RawMemoAttributeAreEqualFn from '!!raw-loader!../../feliz-docs/Examples/Feliz/ReactMemoComponentAreEqualFn.fs'
214+
215+
<ComponentRender code={RawMemoAttributeAreEqualFn} defaultOpen>
216+
<MemoAttributeAreEqualFn />
217+
</ComponentRender>
218+
219+
### areEqual - JavaScript code emit
203220

204221
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.
205222

@@ -242,7 +259,7 @@ import RawMemoAttributeAreEqualEmit from '!!raw-loader!../../feliz-docs/Examples
242259
<MemoAttributeAreEqualEmit />
243260
</ComponentRender>
244261

245-
### areEqual with F# function
262+
### areEqual - F# function call emit
246263

247264
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.
248265

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
hide_title: true
3+
---
4+
5+
import MarkdownFetcher from '@site/src/components/MarkdownFetcher';
6+
7+
<MarkdownFetcher
8+
mdSource='https://raw.githubusercontent.com/glutinum-org/cli/refs/heads/main/README.md'
9+
name='Glutinum'
10+
github='https://github.com/glutinum-org/cli'
11+
docs='https://github.com/glutinum-org/cli?tab=readme-ov-file#glutinumcli'
12+
children={<>
13+
Glutinum is a simple and easy-to-use tool, allowing you to convert <code>.d.ts</code> files to F# bindings directly from your browser without installing anything.
14+
15+
Check out the handy online converter at <a href="https://glutinum.net/" target='_blank'>https://glutinum.net/</a> to get started quickly!
16+
</>}
17+
/>

docs/docs/ecosystem/02_UI/tailwindcss.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Because interop with tailwind and F# is so quick and easy the [Feliz template](.
1818

1919
You can easily configure the [tailwindcss VS Code extension](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) to provide autocomplete and intellisense features for F# files. Check out the autocomplete and intellisense features from the image below!
2020

21+
:::note[YOUR HELP NEEDED ❤️]
22+
If anyone is using a different editor and knows how to set this up there, please(!) let me know!
23+
:::
24+
2125
![Image showing autocomplete and intellisense features of tailwindcss vs code plugin for F#](@site/static/img/tailwind_vs_code_plugin.png)
2226

2327
You just need to add the following to your VS Code settings.json:
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module Examples.Feliz.ReactMemoComponentAreEqualFn
2+
3+
open Feliz
4+
5+
// Memo using F# equality but ignoring function properties
6+
[<ReactMemoComponent(AreEqualFn.FsEqualsButFunctions)>]
7+
let MemoFsEqualsButFunctions (values: int list, fn: int -> int) =
8+
let renderCount = React.useRef 0
9+
renderCount.current <- renderCount.current + 1
10+
Html.div [
11+
prop.testId "memo-fs-bf-count";
12+
prop.text (string renderCount.current)
13+
]
14+
15+
// This will rerender when parent renders due to function property change
16+
[<ReactMemoComponent(AreEqualFn.FsEquals)>]
17+
let MemoFsEquals (values: int list, fn: int -> int) =
18+
let renderCount = React.useRef 0
19+
renderCount.current <- renderCount.current + 1
20+
Html.div [
21+
prop.testId "memo2-fs-bf-count";
22+
prop.text (string renderCount.current)
23+
]
24+
25+
// This will alway rerender when parent renders
26+
[<ReactComponent>]
27+
let ComparisonComponent (values: int list, fn: int -> int) =
28+
let renderCount = React.useRef 0
29+
renderCount.current <- renderCount.current + 1
30+
Html.div [
31+
prop.testId "fs-bf-count";
32+
prop.text (string renderCount.current)
33+
]
34+
35+
[<ReactComponent(true)>]
36+
let ParentWithFnFsButFunctions() =
37+
let state, setState = React.useState 0
38+
let fn = fun x -> x + state // New function instance on each render
39+
let values = [ 1; 2 ] // New list instance on each render
40+
Html.div [
41+
Html.button [
42+
prop.testId "btn-fn-bf";
43+
prop.onClick (fun _ -> setState(state + 1));
44+
prop.text "Trigger Rerender by changing state of parent!"
45+
]
46+
MemoFsEqualsButFunctions (values, fn)
47+
MemoFsEquals (values, fn)
48+
ComparisonComponent (values, fn)
49+
]

docs/docs/feliz-docs/Feliz.Docs.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Compile Include="Examples/Feliz/ReactComponentImport.fs" />
6363
<Compile Include="Examples/Feliz/ReactLazyComponent.fs" />
6464
<Compile Include="Examples/Feliz/ReactLazyComponentPath.fs" />
65+
<Compile Include="Examples/Feliz/ReactMemoComponentAreEqualFn.fs" />
6566
<Compile Include="Examples/Guides/UnboxStringEnum.fs" />
6667
<Compile Include="Examples/Guides/BrowserTypesEvents.fs" />
6768
<Compile Include="Examples/Guides/BrowserTypesGlobalNamespaces.fs" />

playground/src/Components.fs

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,85 @@ open Shared
88

99
type jsx = JSX.Html
1010

11-
let private areEqualFn = fun prop1 prop2 -> prop1 = prop2
12-
13-
[<Emit("\"use memo\"")>]
14-
let use_memo: unit = jsNative
15-
16-
[<ReactMemoComponent(areEqual="(a,b) => a.fruitArray === b.fruitArray")>]
17-
let private MemoAttributeAreEqualJsEqual (fruitArray: string []) =
18-
use_memo
19-
React.useEffect (fun () -> console.log("MemoAttributeAreEqualJsEqual rendered"))
20-
Html.div [
21-
prop.text (String.concat ", " fruitArray);
22-
prop.testId "memo-attribute"
23-
]
11+
// let private areEqualFn = fun prop1 prop2 -> prop1 = prop2
12+
13+
// [<Emit("\"use memo\"")>]
14+
// let use_memo: unit = jsNative
15+
16+
17+
18+
module Components =
19+
20+
// Memo using F# structural equality (deep equality)
21+
[<ReactMemoComponent(AreEqualFn.FsEquals)>]
22+
let MemoFsEquals (values: int list) =
23+
let renderCount = React.useRef 0
24+
renderCount.current <- renderCount.current + 1
25+
Html.div [ prop.testId "memo-fs-count"; prop.text (string renderCount.current) ]
26+
27+
// Memo using shallow (JS) equality
28+
[<ReactMemoComponent()>]
29+
let MemoShallow (values: int list) =
30+
let renderCount = React.useRef 0
31+
renderCount.current <- renderCount.current + 1
32+
Html.div [ prop.testId "memo-shallow-count"; prop.text (string renderCount.current) ]
33+
34+
// Memo using F# equality and a function prop (for testing function handling)
35+
[<ReactMemoComponent(AreEqualFn.FsEquals)>]
36+
let MemoFsEqualsWithFn (values: int list, fn: int -> int) =
37+
let renderCount = React.useRef 0
38+
renderCount.current <- renderCount.current + 1
39+
Html.div [ prop.testId "memo-fs-fn-count"; prop.text (string renderCount.current) ]
40+
41+
// Memo using F# equality but ignoring function properties
42+
[<ReactMemoComponent(AreEqualFn.FsEqualsButFunctions)>]
43+
let MemoFsEqualsButFunctions (values: int list, fn: int -> int) =
44+
let renderCount = React.useRef 0
45+
renderCount.current <- renderCount.current + 1
46+
Html.div [ prop.testId "memo-fs-bf-count"; prop.text (string renderCount.current) ]
47+
48+
// Parent components used by tests to trigger re-renders with new prop instances
49+
[<ReactComponent>]
50+
let ParentListFsEquals() =
51+
let state, setState = React.useState 0
52+
let values = [ 1; 2 ] // recreated each render (new instance)
53+
Html.div [
54+
Html.button [ prop.testId "btn-list-fs"; prop.onClick (fun _ -> setState(state + 1)); prop.text "Tick" ]
55+
MemoFsEquals values
56+
]
57+
58+
[<ReactComponent>]
59+
let ParentListShallow() =
60+
let state, setState = React.useState 0
61+
let values = [ 1; 2 ]
62+
Html.div [
63+
Html.button [ prop.testId "btn-list-shallow"; prop.onClick (fun _ -> setState(state + 1)); prop.text "Tick" ]
64+
MemoShallow values
65+
]
66+
67+
[<ReactComponent>]
68+
let ParentWithFnFs() =
69+
let state, setState = React.useState 0
70+
// create a new function each render (captures state) so its reference is different
71+
let fn = fun x -> x + state
72+
let values = [ 1; 2 ]
73+
Html.div [
74+
Html.button [ prop.testId "btn-fn-fs"; prop.onClick (fun _ -> setState(state + 1)); prop.text "Tick" ]
75+
MemoFsEqualsWithFn (values, fn)
76+
]
77+
78+
[<ReactComponent>]
79+
let ParentWithFnFsButFunctions() =
80+
let state, setState = React.useState 0
81+
let fn = fun x -> x + state
82+
let values = [ 1; 2 ]
83+
Html.div [
84+
Html.button [ prop.testId "btn-fn-bf"; prop.onClick (fun _ -> setState(state + 1)); prop.text "Tick" ]
85+
MemoFsEqualsButFunctions (values, fn)
86+
]
87+
88+
2489

25-
2690
[<ReactComponent>]
2791
let Main () =
2892
let fruitArray, setFruitArray = React.useState ([| "Apple"; "Banana"; "Orange" |]) // This stays the same array
@@ -38,20 +102,20 @@ let Main () =
38102
style.color "white"
39103
]
40104
prop.children [
41-
Html.button [
42-
prop.testId "change-fruit-array"
43-
prop.text "Change Fruit Array"
44-
prop.onClick (fun _ -> setFruitArray [| "Apple"; "Banana"; "Orange"; "Cherry" |])
45-
]
46-
Html.button [
47-
prop.testId "change-theme"
48-
prop.text "Change Theme"
49-
prop.onClick (fun _ ->
50-
if theme = "light" then
51-
setTheme "dark"
52-
else
53-
setTheme "light")
54-
]
55-
MemoAttributeAreEqualJsEqual (sortedFruits)
105+
// Html.button [
106+
// prop.testId "change-fruit-array"
107+
// prop.text "Change Fruit Array"
108+
// prop.onClick (fun _ -> setFruitArray [| "Apple"; "Banana"; "Orange"; "Cherry" |])
109+
// ]
110+
// Html.button [
111+
// prop.testId "change-theme"
112+
// prop.text "Change Theme"
113+
// prop.onClick (fun _ ->
114+
// if theme = "light" then
115+
// setTheme "dark"
116+
// else
117+
// setTheme "light")
118+
// ]
119+
Components.ParentWithFnFsButFunctions()
56120
]
57121
]

src/Feliz.CompilerPlugins/AstUtils.fs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Feliz.AstUtils
22

33
open Fable
44
open Fable.AST
5+
open Fable.AST.Fable
56
open System
67
open System.Linq
78
open System.Text.RegularExpressions
@@ -42,6 +43,32 @@ let makeCallInfo args: Fable.CallInfo = {
4243
Tags = []
4344
}
4445

46+
module ImportFromFableLib =
47+
48+
let private getLibPath (com: PluginHelper) (moduleName: string) =
49+
match com.Options.Language with
50+
| TypeScript -> com.LibraryDir + "/" + moduleName + ".ts"
51+
| JavaScript -> com.LibraryDir + "/" + moduleName + ".js"
52+
| _ ->
53+
failwith "Only JavaScript and TypeScript are supported"
54+
55+
let private makeImportLibWithInfo (com: PluginHelper) t memberName (moduleName: string) info =
56+
let selector = memberName
57+
58+
Import(
59+
{
60+
Selector = selector
61+
Path = getLibPath com moduleName
62+
Kind = LibraryImport info
63+
},
64+
t,
65+
None
66+
)
67+
68+
let makeImportLib (com: PluginHelper) t memberName moduleName =
69+
Fable.AST.Fable.LibraryImportInfo.Create(isInstanceMember = false, isModuleMember = true)
70+
|> makeImportLibWithInfo com t memberName moduleName
71+
4572
let emitJs macro args =
4673
let callInfo = makeCallInfo args
4774

src/Feliz.CompilerPlugins/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
## [Unreleased]
1818

19+
## 3.0.0-rc.8 - 2025-11-28
20+
21+
### ✨ Added
22+
23+
- Support for predefined equality functions for `[<ReactMemoComponent>]` (by @Freymaurer, @melanore)
24+
1925
## 3.0.0-rc.7 - 2025-11-26
2026

2127
### 🗑️ Deprecated

src/Feliz.CompilerPlugins/PrimitiveElement.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ open Fable.AST
1515
// | _ ->
1616
// expr
1717
//
18-
// override this.Transform(logger, decl) = decl
18+
// override this.Transform(logger, decl) = decl

0 commit comments

Comments
 (0)