Skip to content

Commit e7494bd

Browse files
New hooks (#66)
* Add useId * Add useTransition * Add useDeferredValue * Add useSyncExternalStore * Add useInsertionEffect * Fix signatures * Drop vendored Discovery * Add tests for new hooks * Fix docstring * Make test less flaky * remove redundant import
1 parent 65d87a4 commit e7494bd

File tree

8 files changed

+273
-50
lines changed

8 files changed

+273
-50
lines changed

packages.dhall

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
let upstream =
2-
https://github.com/purescript/package-sets/releases/download/psc-0.15.0-20220527/packages.dhall
3-
sha256:15dd8041480502850e4043ea2977ed22d6ab3fc24d565211acde6f8c5152a799
2+
https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220531/packages.dhall
3+
sha256:278d3608439187e51136251ebf12fabda62d41ceb4bec9769312a08b56f853e3
44

55
in upstream
66
with react-testing-library =

spago.test.dhall

+7
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@ in conf // {
66
[ "react-testing-library"
77
, "react-basic-dom"
88
, "spec"
9+
, "spec-discovery"
10+
, "foreign-object"
11+
, "web-dom"
12+
, "arrays"
13+
, "strings"
14+
, "debug"
15+
, "tailrec"
916
]
1017
}

src/React/Basic/Hooks.js

+22
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ export function useLayoutEffectAlways_(effect) {
4444
return React.useLayoutEffect(effect);
4545
}
4646

47+
export function useInsertionEffect_(eq, deps, effect) {
48+
const memoizedKey = useEqCache(eq, deps);
49+
React.useInsertionEffect(effect, [memoizedKey]);
50+
}
51+
52+
export function useInsertionEffectAlways_(effect) {
53+
React.useInsertionEffect(effect);
54+
}
55+
4756
export function useReducer_(tuple, reducer, initialState) {
4857
const [state, dispatch] = React.useReducer(reducer, initialState);
4958
if (!dispatch.hasOwnProperty("$$reactBasicHooks$$cachedDispatch")) {
@@ -73,6 +82,19 @@ export function useMemo_(eq, deps, computeA) {
7382

7483
export const useDebugValue_ = React.useDebugValue;
7584

85+
export const useId_ = React.useId
86+
87+
export function useTransition_(tuple) {
88+
const [isPending, startTransitionImpl] = React.useTransition()
89+
const startTransition = (update) => () => startTransitionImpl(update)
90+
return tuple(isPending, startTransition);
91+
}
92+
93+
export const useDeferredValue_ = React.useDeferredValue
94+
95+
export const useSyncExternalStore2_ = React.useSyncExternalStore
96+
export const useSyncExternalStore3_ = React.useSyncExternalStore
97+
7698
export function unsafeSetDisplayName(displayName, component) {
7799
component.displayName = displayName;
78100
component.toString = () => displayName;

src/React/Basic/Hooks.purs

+98
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ module React.Basic.Hooks
2020
, useLayoutEffectOnce
2121
, useLayoutEffectAlways
2222
, UseLayoutEffect
23+
, useInsertionEffect
24+
, useInsertionEffectOnce
25+
, useInsertionEffectAlways
26+
, UseInsertionEffect
2327
, Reducer
2428
, mkReducer
2529
, runReducer
@@ -38,6 +42,15 @@ module React.Basic.Hooks
3842
, UseMemo
3943
, useDebugValue
4044
, UseDebugValue
45+
, useId
46+
, UseId
47+
, useTransition
48+
, UseTransition
49+
, useDeferredValue
50+
, UseDeferredValue
51+
, useSyncExternalStore
52+
, useSyncExternalStore'
53+
, UseSyncExternalStore
4154
, UnsafeReference(..)
4255
, displayName
4356
, module React.Basic.Hooks.Internal
@@ -268,6 +281,26 @@ useLayoutEffectAlways effect = unsafeHook (runEffectFn1 useLayoutEffectAlways_ e
268281

269282
foreign import data UseLayoutEffect :: Type -> Type -> Type
270283

284+
useInsertionEffect ::
285+
forall deps.
286+
Eq deps =>
287+
deps ->
288+
Effect (Effect Unit) ->
289+
Hook (UseInsertionEffect deps) Unit
290+
useInsertionEffect deps effect = unsafeHook (runEffectFn3 useInsertionEffect_ (mkFn2 eq) deps effect)
291+
292+
--| Like `useInsertionEffect`, but the effect is only performed a single time per component
293+
--| instance. Prefer `useInsertionEffect` with a proper dependency list whenever possible!
294+
useInsertionEffectOnce :: Effect (Effect Unit) -> Hook (UseInsertionEffect Unit) Unit
295+
useInsertionEffectOnce effect = unsafeHook (runEffectFn3 useInsertionEffect_ (mkFn2 \_ _ -> true) unit effect)
296+
297+
--| Like `useInsertionEffect`, but the effect is performed on every render. Prefer `useInsertionEffect`
298+
--| with a proper dependency list whenever possible!
299+
useInsertionEffectAlways :: Effect (Effect Unit) -> Hook (UseInsertionEffect Unit) Unit
300+
useInsertionEffectAlways effect = unsafeHook (runEffectFn1 useInsertionEffectAlways_ effect)
301+
302+
foreign import data UseInsertionEffect :: Type -> Type -> Type
303+
271304
newtype Reducer state action
272305
= Reducer (Fn2 state action state)
273306

@@ -354,6 +387,39 @@ useDebugValue debugValue display = unsafeHook (runEffectFn2 useDebugValue_ debug
354387

355388
foreign import data UseDebugValue :: Type -> Type -> Type
356389

390+
foreign import data UseId :: Type -> Type
391+
useId :: Hook UseId String
392+
useId = unsafeHook useId_
393+
394+
foreign import data UseTransition :: Type -> Type
395+
useTransition ::
396+
Hook UseTransition (Boolean /\ ((Effect Unit) -> Effect Unit))
397+
useTransition = unsafeHook $ runEffectFn1 useTransition_ (mkFn2 Tuple)
398+
399+
foreign import data UseDeferredValue :: Type -> Type -> Type
400+
useDeferredValue :: forall a. a -> Hook (UseDeferredValue a) a
401+
useDeferredValue a = unsafeHook $ runEffectFn1 useDeferredValue_ a
402+
403+
foreign import data UseSyncExternalStore :: Type -> Type -> Type
404+
useSyncExternalStore :: forall a.
405+
((Effect Unit) -> Effect (Effect Unit))
406+
-> (Effect a)
407+
-> (Effect a)
408+
-> Hook (UseSyncExternalStore a) a
409+
useSyncExternalStore subscribe getSnapshot getServerSnapshot =
410+
unsafeHook $
411+
runEffectFn3 useSyncExternalStore3_
412+
(mkEffectFn1 subscribe)
413+
getSnapshot
414+
getServerSnapshot
415+
useSyncExternalStore' :: forall a.
416+
((Effect Unit) -> Effect (Effect Unit))
417+
-> (Effect a)
418+
-> Hook (UseSyncExternalStore a) a
419+
useSyncExternalStore' subscribe getSnapshot =
420+
unsafeHook $
421+
runEffectFn2 useSyncExternalStore2_ (mkEffectFn1 subscribe) getSnapshot
422+
357423
newtype UnsafeReference a
358424
= UnsafeReference a
359425

@@ -424,6 +490,19 @@ foreign import useLayoutEffectAlways_ ::
424490
(Effect (Effect Unit))
425491
Unit
426492

493+
foreign import useInsertionEffect_ ::
494+
forall deps.
495+
EffectFn3
496+
(Fn2 deps deps Boolean)
497+
deps
498+
(Effect (Effect Unit))
499+
Unit
500+
501+
foreign import useInsertionEffectAlways_ ::
502+
EffectFn1
503+
(Effect (Effect Unit))
504+
Unit
505+
427506
foreign import useReducer_ ::
428507
forall state action.
429508
EffectFn3
@@ -478,3 +557,22 @@ foreign import useDebugValue_ ::
478557
a
479558
(a -> String)
480559
Unit
560+
561+
foreign import useId_ :: Effect String
562+
563+
foreign import useTransition_
564+
:: forall a b. EffectFn1 (Fn2 a b (a /\ b))
565+
(Boolean /\ ((Effect Unit) -> Effect Unit))
566+
567+
foreign import useDeferredValue_ :: forall a. EffectFn1 a a
568+
569+
foreign import useSyncExternalStore2_ :: forall a. EffectFn2
570+
(EffectFn1 (Effect Unit) (Effect Unit)) -- subscribe
571+
(Effect a) -- getSnapshot
572+
a
573+
574+
foreign import useSyncExternalStore3_ :: forall a. EffectFn3
575+
(EffectFn1 (Effect Unit) (Effect Unit)) -- subscribe
576+
(Effect a) -- getSnapshot
577+
(Effect a) -- getServerSnapshot
578+
a

test/Discovery.js

-26
This file was deleted.

test/Discovery.purs

-21
This file was deleted.

test/Main.purs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Data.Maybe (Maybe(..))
66
import Data.Time.Duration (Seconds(..), fromDuration)
77
import Effect (Effect)
88
import Effect.Aff (delay, launchAff_)
9-
import Test.Discovery (discover)
9+
import Test.Spec.Discovery (discover)
1010
import Test.Spec.Reporter (consoleReporter)
1111
import Test.Spec.Runner (defaultConfig, runSpec')
1212

test/Spec/React18HooksSpec.purs

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
module Test.Spec.React18HooksSpec where
2+
3+
import Prelude
4+
5+
import Control.Monad.Rec.Class (forever)
6+
import Data.Array as Array
7+
import Data.Foldable (for_, traverse_)
8+
import Data.Maybe (fromMaybe)
9+
import Data.Monoid (guard, power)
10+
import Data.String as String
11+
import Data.Tuple.Nested ((/\))
12+
import Effect.Aff (Milliseconds(..), apathize, delay, launchAff_)
13+
import Effect.Class (liftEffect)
14+
import Effect.Ref as Ref
15+
import Foreign.Object as Object
16+
import React.Basic (fragment)
17+
import React.Basic.DOM as R
18+
import React.Basic.DOM.Events (targetValue)
19+
import React.Basic.Events (handler, handler_)
20+
import React.Basic.Hooks (reactComponent)
21+
import React.Basic.Hooks as Hooks
22+
import React.TestingLibrary (cleanup, fireEventClick, renderComponent, typeText)
23+
import Test.Spec (Spec, after_, before, describe, it)
24+
import Test.Spec.Assertions (shouldNotEqual)
25+
import Test.Spec.Assertions.DOM (textContentShouldEqual)
26+
import Web.DOM.Element (getAttribute)
27+
import Web.HTML.HTMLElement as HTMLElement
28+
29+
spec Spec Unit
30+
spec =
31+
after_ cleanup do
32+
before setup do
33+
describe "React 18 hooks" do
34+
it "useId works" \{ useId } -> do
35+
{ findByTestId } <- renderComponent useId {}
36+
elem <- findByTestId "use-id"
37+
idʔ <- getAttribute "id" (HTMLElement.toElement elem) # liftEffect
38+
let id = idʔ # fromMaybe ""
39+
id `shouldNotEqual` ""
40+
elem `textContentShouldEqual` id
41+
42+
it "useTransition works" \{ useTransition } -> do
43+
{ findByText } <- renderComponent useTransition {}
44+
elem <- findByText "0"
45+
fireEventClick elem
46+
elem `textContentShouldEqual` "1"
47+
48+
it "useDeferredValue hopefully works" \{ useDeferredValue } -> do
49+
{ findByTestId } <- renderComponent useDeferredValue {}
50+
spanElem <- findByTestId "span"
51+
spanElem `textContentShouldEqual` "0"
52+
findByTestId "input" >>= typeText (power "text" 100)
53+
spanElem `textContentShouldEqual` "400"
54+
55+
it "useSyncExternalStore" \{ useSyncExternalStore } -> do
56+
{ findByTestId } <- renderComponent useSyncExternalStore {}
57+
spanElem <- findByTestId "span"
58+
spanElem `textContentShouldEqual` "0"
59+
delay (350.0 # Milliseconds)
60+
spanElem `textContentShouldEqual` "3"
61+
62+
it "useInsertionEffect works" \{ useInsertionEffect } -> do
63+
{ findByText } <- renderComponent useInsertionEffect {}
64+
void $ findByText "insertion-done"
65+
66+
where
67+
setup = liftEffect ado
68+
69+
useId <-
70+
reactComponent "UseIDExample" \(_ :: {}) -> Hooks.do
71+
id <- Hooks.useId
72+
pure $ R.div
73+
{ id
74+
, _data: Object.singleton "testid" "use-id"
75+
, children: [ R.text id ]
76+
}
77+
78+
useTransition <-
79+
reactComponent "UseTransitionExample" \(_ :: {}) -> Hooks.do
80+
isPending /\ startTransition <- Hooks.useTransition
81+
count /\ setCount <- Hooks.useState 0
82+
let handleClick = startTransition do setCount (_ + 1)
83+
pure $ R.div
84+
{ children:
85+
[ guard isPending (R.text "Pending")
86+
, R.button
87+
{ onClick: handler_ handleClick
88+
, children: [ R.text (show count) ]
89+
}
90+
]
91+
}
92+
93+
useDeferredValue <-
94+
reactComponent "UseDeferredValueExample" \(_ :: {}) -> Hooks.do
95+
text /\ setText <- Hooks.useState' ""
96+
textLength <- Hooks.useDeferredValue (String.length text)
97+
pure $ fragment
98+
[ R.input
99+
{ onChange: handler targetValue (traverse_ setText)
100+
, _data: Object.singleton "testid" "input"
101+
}
102+
, R.span
103+
{ _data: Object.singleton "testid" "span"
104+
, children: [ R.text (show textLength) ]
105+
}
106+
]
107+
108+
useInsertionEffect <-
109+
reactComponent "UseInsertionEffectExample" \(_ :: {}) -> Hooks.do
110+
text /\ setText <- Hooks.useState' "waiting"
111+
Hooks.useInsertionEffect unit do
112+
setText "insertion-done"
113+
mempty
114+
pure $ R.span_ [ R.text text ]
115+
116+
useSyncExternalStore <- do
117+
{ subscribe, getSnapshot, getServerSnapshot } <- do
118+
subscribersRef <- Ref.new []
119+
intRef <- Ref.new 0
120+
-- Update the intRef every 100ms.
121+
launchAff_ $ apathize $ forever do
122+
delay (100.0 # Milliseconds)
123+
intRef # Ref.modify_ (_ + 1) # liftEffect
124+
subscribers <- subscribersRef # Ref.read # liftEffect
125+
liftEffect $ for_ subscribers identity
126+
127+
pure
128+
{ subscribe: \callback -> do
129+
subscribersRef # Ref.modify_ (Array.cons callback)
130+
pure $
131+
subscribersRef # Ref.modify_ (Array.drop 1)
132+
, getSnapshot: Ref.read intRef
133+
, getServerSnapshot: Ref.read intRef
134+
}
135+
136+
reactComponent "UseSyncExternalStoreExample" \(_ :: {}) -> Hooks.do
137+
number <- Hooks.useSyncExternalStore
138+
subscribe
139+
getSnapshot
140+
getServerSnapshot
141+
pure $ R.span { _data: Object.singleton "testid" "span", children: [ R.text (show number) ] }
142+
143+
in { useId, useTransition, useDeferredValue, useInsertionEffect, useSyncExternalStore }

0 commit comments

Comments
 (0)