From 887b90b00d1922ed8326db8f9edc9a08cb55e3fb Mon Sep 17 00:00:00 2001 From: maddie <715921+megamaddu@users.noreply.github.com> Date: Tue, 24 May 2022 00:27:54 -0700 Subject: [PATCH] memo' tests --- .github/workflows/node.js.yml | 8 +- bower.json | 91 +++++----- packages.dhall | 14 +- src/React/Basic/Hooks.purs | 162 +++++++++-------- src/React/Basic/Hooks/Aff.purs | 58 +++--- src/React/Basic/Hooks/ErrorBoundary.purs | 6 +- src/React/Basic/Hooks/Internal.purs | 204 +++++++++++----------- src/React/Basic/Hooks/ResetToken.purs | 18 +- src/React/Basic/Hooks/Suspense.purs | 34 ++-- src/React/Basic/Hooks/Suspense/Store.purs | 2 +- test/Spec/MemoSpec.purs | 106 +++++++++++ 11 files changed, 402 insertions(+), 301 deletions(-) create mode 100644 test/Spec/MemoSpec.purs diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c90f774..2034614 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,17 +13,11 @@ jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [12.x, 14.x, 16.x, 18.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - steps: - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node-version }} cache: "npm" - run: npm ci - run: npm run deps --if-present diff --git a/bower.json b/bower.json index 0c3b116..cac710b 100644 --- a/bower.json +++ b/bower.json @@ -1,51 +1,44 @@ { - "name": "purescript-react-basic-hooks", - "license": [ - "Apache-2.0" - ], - "repository": { - "type": "git", - "url": "https://github.com/spicydonuts/purescript-react-basic-hooks" - }, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "output" - ], - "dependencies": { - "purescript-aff": "^v7.0.0", - "purescript-aff-promise": "^v4.0.0", - "purescript-bifunctors": "^v6.0.0", - "purescript-console": "^v6.0.0", - "purescript-control": "^v6.0.0", - "purescript-datetime": "^v6.0.0", - "purescript-effect": "^v4.0.0", - "purescript-either": "^v6.0.0", - "purescript-exceptions": "^v6.0.0", - "purescript-foldable-traversable": "^v6.0.0", - "purescript-functions": "^v6.0.0", - "purescript-indexed-monad": "^v2.1.0", - "purescript-integers": "^v6.0.0", - "purescript-maybe": "^v6.0.0", - "purescript-newtype": "^v5.0.0", - "purescript-now": "^v6.0.0", - "purescript-nullable": "^v6.0.0", - "purescript-ordered-collections": "^v3.0.0", - "purescript-prelude": "^v6.0.0", - "purescript-react-basic": "^v17.0.0", - "purescript-refs": "^v6.0.0", - "purescript-tuples": "^v7.0.0", - "purescript-type-equality": "^v4.0.1", - "purescript-unsafe-coerce": "^v6.0.0", - "purescript-unsafe-reference": "^v5.0.0", - "purescript-web-html": "^v4.0.0" - }, - "resolutions": { - "purescript-control": "^6.0.0", - "purescript-prelude": "^6.0.0", - "purescript-newtype": "^5.0.0", - "purescript-unsafe-coerce": "^6.0.0", - "purescript-safe-coerce": "^2.0.0" - } + "name": "purescript-react-basic-hooks", + "license": [ + "Apache-2.0" + ], + "repository": { + "type": "git", + "url": "https://github.com/spicydonuts/purescript-react-basic-hooks" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "output" + ], + "dependencies": { + "purescript-aff": "^v7.0.0", + "purescript-aff-promise": "^v4.0.0", + "purescript-bifunctors": "^v6.0.0", + "purescript-console": "^v6.0.0", + "purescript-control": "^v6.0.0", + "purescript-datetime": "^v6.0.0", + "purescript-effect": "^v4.0.0", + "purescript-either": "^v6.0.0", + "purescript-exceptions": "^v6.0.0", + "purescript-foldable-traversable": "^v6.0.0", + "purescript-functions": "^v6.0.0", + "purescript-indexed-monad": "^v2.1.0", + "purescript-integers": "^v6.0.0", + "purescript-maybe": "^v6.0.0", + "purescript-newtype": "^v5.0.0", + "purescript-now": "^v6.0.0", + "purescript-nullable": "^v6.0.0", + "purescript-ordered-collections": "^v3.0.0", + "purescript-prelude": "^v6.0.0", + "purescript-react-basic": "^v17.0.0", + "purescript-refs": "^v6.0.0", + "purescript-tuples": "^v7.0.0", + "purescript-type-equality": "^v4.0.1", + "purescript-unsafe-coerce": "^v6.0.0", + "purescript-unsafe-reference": "^v5.0.0", + "purescript-web-html": "^v4.0.0" + } } diff --git a/packages.dhall b/packages.dhall index d48d652..01140c4 100644 --- a/packages.dhall +++ b/packages.dhall @@ -1,8 +1,8 @@ let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.15.0-20220522/packages.dhall - sha256:43895efaec7af246b60b59cfbf451cd9d3d84a5327de8c0945e2de5c9fd2fcf2 + https://github.com/purescript/package-sets/releases/download/psc-0.15.0-20220523/packages.dhall + sha256:985f90fa68fd8b43b14c777d6ec2c161c4dd9009563b6f51685a54e4a26bf8ff -in upstream +in upstream with react-testing-library = { dependencies = [ "aff" @@ -27,9 +27,8 @@ in upstream ] , repo = "https://github.com/i-am-the-slime/purescript-react-testing-library" - , version = "v4.0.0" + , version = "v4.0.1" } - with react-basic-dom = { dependencies = [ "effect" @@ -45,7 +44,6 @@ in upstream , "web-file" , "web-html" ] - , repo = - "https://github.com/Zelenya7/purescript-react-basic-dom" + , repo = "https://github.com/Zelenya7/purescript-react-basic-dom" , version = "purescript-0.15-spago" - } \ No newline at end of file + } diff --git a/src/React/Basic/Hooks.purs b/src/React/Basic/Hooks.purs index 02470e2..5cea930 100644 --- a/src/React/Basic/Hooks.purs +++ b/src/React/Basic/Hooks.purs @@ -63,15 +63,15 @@ import React.Basic.Hooks.Internal (Hook, HookApply, Pure, Render, bind, discard, import Unsafe.Coerce (unsafeCoerce) import Unsafe.Reference (unsafeRefEq) --- | A simple type alias to clean up component definitions. +--| A simple type alias to clean up component definitions. type Component props = Effect (props -> JSX) --- | Create a component function given a display name and render function. --- | Creating components is effectful because React uses the function --- | instance as the component's "identity" or "type". Components should --- | be created during a bootstrap phase and not within component --- | lifecycles or render functions. +--| Create a component function given a display name and render function. +--| Creating components is effectful because React uses the function +--| instance as the component's "identity" or "type". Components should +--| be created during a bootstrap phase and not within component +--| lifecycles or render functions. component :: forall hooks props. String -> @@ -81,12 +81,12 @@ component name renderFn = Prelude.do c <- reactComponent name (renderFn <<< _.nested) pure (element c <<< { nested: _ }) --- | Create a React component given a display name and render function. --- | Creating components is effectful because React uses the function --- | instance as the component's "identity" or "type". Components should --- | be created during a bootstrap phase and not within component --- | lifecycles or render functions. See `componentWithChildren` if --- | you need to use the `children` prop. +--| Create a React component given a display name and render function. +--| Creating components is effectful because React uses the function +--| instance as the component's "identity" or "type". Components should +--| be created during a bootstrap phase and not within component +--| lifecycles or render functions. See `componentWithChildren` if +--| you need to use the `children` prop. reactComponent :: forall hooks props. Lacks "children" props => @@ -97,9 +97,9 @@ reactComponent :: Effect (ReactComponent { | props }) reactComponent = unsafeReactComponent --- | Create a React component given a display name and render function. --- | This is the same as `component` but allows the use of the `children` --- | prop. +--| Create a React component given a display name and render function. +--| This is the same as `component` but allows the use of the `children` +--| prop. reactComponentWithChildren :: forall hooks props children. Lacks "key" props => @@ -109,11 +109,11 @@ reactComponentWithChildren :: Effect (ReactComponent { children :: ReactChildren children | props }) reactComponentWithChildren = unsafeReactComponent --- | Convert a hook to a render-prop component. The value returned from the --- | hook will be passed to the `render` prop, a function from that value --- | to `JSX`. --- | --- | This function is useful for consuming a hook within a non-hook component. +--| Convert a hook to a render-prop component. The value returned from the +--| hook will be passed to the `render` prop, a function from that value +--| to `JSX`. +--| +--| This function is useful for consuming a hook within a non-hook component. reactComponentFromHook :: forall hooks props r. Lacks "children" props => @@ -158,16 +158,26 @@ foreign import reactChildrenToArray :: forall a. ReactChildren a -> Array a reactChildrenFromArray :: forall a. Array a -> ReactChildren a reactChildrenFromArray = unsafeCoerce --- | Prevents a component from re-rendering if its new props are referentially --- | equal to its old props (not value-based equality -- this is due to the --- | underlying React implementation). +--| Prevents a component from re-rendering if its new props are referentially +--| equal to its old props (not value-based equality -- this is due to the +--| underlying React implementation). +--| Prefer `memo'` for more PureScript-friendldy behavior. memo :: forall props. Effect (ReactComponent props) -> Effect (ReactComponent props) memo = flip Prelude.bind (runEffectFn1 memo_) --- | Similar to `memo` but takes a function to compare previous and new props +--| Similar to `memo` but takes a function to compare previous and new props. +--| For example: +--| +--| ```purs +--| mkMyComponent :: Effect (ReactComponent { id :: Int }) +--| mkMyComponent = +--| memo' eq do +--| reactComponent "MyComponent" \{ id } -> React.do +--| ... +--| ``` memo' :: forall props. (props -> props -> Boolean) -> @@ -193,25 +203,25 @@ useState' initialState = useState initialState <#> rmap (_ <<< const) foreign import data UseState :: Type -> Type -> Type --- | Runs the given effect when the component is mounted and any time the given --- | dependencies change. The effect should return its cleanup function. For --- | example, if the effect registers a global event listener, it should return --- | an Effect which removes the listener. --- | --- | ```purs --- | useEffect deps do --- | timeoutId <- setTimeout 1000 (logShow deps) --- | pure (clearTimeout timeoutId) --- | ``` --- | --- | If no cleanup is needed, use `pure (pure unit)` or `pure mempty` to return --- | a no-op Effect --- | --- | ```purs --- | useEffect deps do --- | logShow deps --- | pure mempty --- | ``` +--| Runs the given effect when the component is mounted and any time the given +--| dependencies change. The effect should return its cleanup function. For +--| example, if the effect registers a global event listener, it should return +--| an Effect which removes the listener. +--| +--| ```purs +--| useEffect deps do +--| timeoutId <- setTimeout 1000 (logShow deps) +--| pure (clearTimeout timeoutId) +--| ``` +--| +--| If no cleanup is needed, use `pure (pure unit)` or `pure mempty` to return +--| a no-op Effect +--| +--| ```purs +--| useEffect deps do +--| logShow deps +--| pure mempty +--| ``` useEffect :: forall deps. Eq deps => @@ -222,22 +232,22 @@ useEffect deps effect = unsafeHook do runEffectFn3 useEffect_ (mkFn2 eq) deps effect --- | Like `useEffect`, but the effect is only performed a single time per component --- | instance. Prefer `useEffect` with a proper dependency list whenever possible! +--| Like `useEffect`, but the effect is only performed a single time per component +--| instance. Prefer `useEffect` with a proper dependency list whenever possible! useEffectOnce :: Effect (Effect Unit) -> Hook (UseEffect Unit) Unit useEffectOnce effect = unsafeHook (runEffectFn3 useEffect_ (mkFn2 \_ _ -> true) unit effect) --- | Like `useEffect`, but the effect is performed on every render. Prefer `useEffect` --- | with a proper dependency list whenever possible! +--| Like `useEffect`, but the effect is performed on every render. Prefer `useEffect` +--| with a proper dependency list whenever possible! useEffectAlways :: Effect (Effect Unit) -> Hook (UseEffect Unit) Unit useEffectAlways effect = unsafeHook (runEffectFn1 useEffectAlways_ effect) foreign import data UseEffect :: Type -> Type -> Type --- | Like `useEffect`, but the effect is performed synchronously after the browser has --- | calculated layout. Useful for reading properties from the DOM that are not available --- | before layout, such as element sizes and positions. Prefer `useEffect` whenever --- | possible to avoid blocking browser painting. +--| Like `useEffect`, but the effect is performed synchronously after the browser has +--| calculated layout. Useful for reading properties from the DOM that are not available +--| before layout, such as element sizes and positions. Prefer `useEffect` whenever +--| possible to avoid blocking browser painting. useLayoutEffect :: forall deps. Eq deps => @@ -246,13 +256,13 @@ useLayoutEffect :: Hook (UseLayoutEffect deps) Unit useLayoutEffect deps effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 eq) deps effect) --- | Like `useLayoutEffect`, but the effect is only performed a single time per component --- | instance. Prefer `useLayoutEffect` with a proper dependency list whenever possible! +--| Like `useLayoutEffect`, but the effect is only performed a single time per component +--| instance. Prefer `useLayoutEffect` with a proper dependency list whenever possible! useLayoutEffectOnce :: Effect (Effect Unit) -> Hook (UseLayoutEffect Unit) Unit useLayoutEffectOnce effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 \_ _ -> true) unit effect) --- | Like `useLayoutEffect`, but the effect is performed on every render. Prefer `useLayoutEffect` --- | with a proper dependency list whenever possible! +--| Like `useLayoutEffect`, but the effect is performed on every render. Prefer `useLayoutEffect` +--| with a proper dependency list whenever possible! useLayoutEffectAlways :: Effect (Effect Unit) -> Hook (UseLayoutEffect Unit) Unit useLayoutEffectAlways effect = unsafeHook (runEffectFn1 useLayoutEffectAlways_ effect) @@ -261,19 +271,19 @@ foreign import data UseLayoutEffect :: Type -> Type -> Type newtype Reducer state action = Reducer (Fn2 state action state) --- | Creating reducer functions for React is effectful because --- | React uses the function instance's reference to optimize --- | rendering behavior. +--| Creating reducer functions for React is effectful because +--| React uses the function instance's reference to optimize +--| rendering behavior. mkReducer :: forall state action. (state -> action -> state) -> Effect (Reducer state action) mkReducer = pure <<< Reducer <<< mkFn2 --- | Run a wrapped `Reducer` function as a normal function (like `runFn2`). --- | Useful for testing, simulating actions, or building more complicated --- | hooks on top of `useReducer` +--| Run a wrapped `Reducer` function as a normal function (like `runFn2`). +--| Useful for testing, simulating actions, or building more complicated +--| hooks on top of `useReducer` runReducer :: forall state action. Reducer state action -> state -> action -> state runReducer (Reducer reducer) = runFn2 reducer --- | Use `mkReducer` to construct a reducer function. +--| Use `mkReducer` to construct a reducer function. useReducer :: forall state action. state -> @@ -309,11 +319,11 @@ useContext context = unsafeHook (runEffectFn1 useContext_ context) foreign import data UseContext :: Type -> Type -> Type --- | Cache an instance of a value, replacing it when `eq` returns `false`. --- | --- | This is a low-level performance optimization tool. It can be useful --- | for optimizing a component's props for use with `memo`, where --- | JavaScript instance equality matters. +--| Cache an instance of a value, replacing it when `eq` returns `false`. +--| +--| This is a low-level performance optimization tool. It can be useful +--| for optimizing a component's props for use with `memo`, where +--| JavaScript instance equality matters. useEqCache :: forall a. Eq a => @@ -325,7 +335,7 @@ useEqCache a = foreign import data UseEqCache :: Type -> Type -> Type --- | Lazily compute a value. The result is cached until the `deps` change. +--| Lazily compute a value. The result is cached until the `deps` change. useMemo :: forall deps a. Eq deps => @@ -338,7 +348,7 @@ useMemo deps computeA = foreign import data UseMemo :: Type -> Type -> Type -> Type --- | Use this hook to display a label for custom hooks in React DevTools +--| Use this hook to display a label for custom hooks in React DevTools useDebugValue :: forall a. a -> (a -> String) -> Hook (UseDebugValue a) Unit useDebugValue debugValue display = unsafeHook (runEffectFn2 useDebugValue_ debugValue display) @@ -352,18 +362,18 @@ derive instance newtypeUnsafeReference :: Newtype (UnsafeReference a) _ instance eqUnsafeReference :: Eq (UnsafeReference a) where eq = unsafeRefEq --- | Retrieve the Display Name from a `ReactComponent`. Useful for debugging and improving --- | error messages in logs. --- | --- | __*See also:* `component`__ +--| Retrieve the Display Name from a `ReactComponent`. Useful for debugging and improving +--| error messages in logs. +--| +--| __*See also:* `component`__ foreign import displayName :: forall props. ReactComponent props -> String --- | --- | Internal utility or FFI functions --- | +--| +--| Internal utility or FFI functions +--| foreign import memo_ :: forall props. EffectFn1 diff --git a/src/React/Basic/Hooks/Aff.purs b/src/React/Basic/Hooks/Aff.purs index ada16ec..45e0bed 100644 --- a/src/React/Basic/Hooks/Aff.purs +++ b/src/React/Basic/Hooks/Aff.purs @@ -23,14 +23,14 @@ import Effect.Unsafe (unsafePerformEffect) import React.Basic.Hooks (type (&), type (/\), Hook, Reducer, UnsafeReference(..), UseEffect, UseMemo, UseReducer, UseState, coerceHook, mkReducer, unsafeRenderEffect, useEffect, useMemo, useReducer, useState, (/\)) import React.Basic.Hooks as React --- | `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect --- | is re-run whenever the deps change. If another `Aff` runs when the deps --- | change before the previous async resolves, it will cancel the previous --- | in-flight effect. --- | --- | *Note: This hook requires parent components to handle error states! Don't --- | forget to implement a React error boundary or avoid `Aff` errors entirely --- | by incorporating them into your result type!* +--| `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect +--| is re-run whenever the deps change. If another `Aff` runs when the deps +--| change before the previous async resolves, it will cancel the previous +--| in-flight effect. +--| +--| *Note: This hook requires parent components to handle error states! Don't +--| forget to implement a React error boundary or avoid `Aff` errors entirely +--| by incorporating them into your result type!* useAff :: forall deps a. Eq deps => @@ -64,24 +64,24 @@ newtype UseAff deps a hooks derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ --- | Provide an initial state and a reducer function. This is a more powerful --- | version of `useReducer`, where a state change can additionally queue --- | asynchronous operations. The results of those operations must be mapped --- | into the reducer's `action` type. This is essentially the Elm architecture. --- | --- | Generally, I recommend `useAff` paired with tools like `useResetToken` over --- | `useAffReducer` as there are many ways `useAffReducer` can result in race --- | conditions. `useAff` with proper dependency management will handle previous --- | request cancellation and ensure your `Aff` result is always in sync with --- | the provided `deps`, for example. To accomplish the same thing with --- | `useAffReducer` would require tracking `Fiber`s manually in your state --- | somehow.. :c --- | --- | That said, `useAffReducer` can still be helpful when converting from the --- | current `React.Basic` (non-hooks) API or for those used to Elm. --- | --- | *Note: Aff failures are thrown. If you need to capture an error state, be --- | sure to capture it in your action type!* +--| Provide an initial state and a reducer function. This is a more powerful +--| version of `useReducer`, where a state change can additionally queue +--| asynchronous operations. The results of those operations must be mapped +--| into the reducer's `action` type. This is essentially the Elm architecture. +--| +--| Generally, I recommend `useAff` paired with tools like `useResetToken` over +--| `useAffReducer` as there are many ways `useAffReducer` can result in race +--| conditions. `useAff` with proper dependency management will handle previous +--| request cancellation and ensure your `Aff` result is always in sync with +--| the provided `deps`, for example. To accomplish the same thing with +--| `useAffReducer` would require tracking `Fiber`s manually in your state +--| somehow.. :c +--| +--| That said, `useAffReducer` can still be helpful when converting from the +--| current `React.Basic` (non-hooks) API or for those used to Elm. +--| +--| *Note: Aff failures are thrown. If you need to capture an error state, be +--| sure to capture it in your action type!* useAffReducer :: forall state action. state -> @@ -133,9 +133,9 @@ mkAffReducer :: Effect (AffReducer state action) mkAffReducer = pure <<< AffReducer <<< mkFn2 --- | Run a wrapped `Reducer` function as a normal function (like `runFn2`). --- | Useful for testing, simulating actions, or building more complicated --- | hooks on top of `useReducer` +--| Run a wrapped `Reducer` function as a normal function (like `runFn2`). +--| Useful for testing, simulating actions, or building more complicated +--| hooks on top of `useReducer` runAffReducer :: forall state action. AffReducer state action -> diff --git a/src/React/Basic/Hooks/ErrorBoundary.purs b/src/React/Basic/Hooks/ErrorBoundary.purs index 7a156c7..dd5849d 100644 --- a/src/React/Basic/Hooks/ErrorBoundary.purs +++ b/src/React/Basic/Hooks/ErrorBoundary.purs @@ -9,9 +9,9 @@ import Effect (Effect) import Effect.Aff (Error) import React.Basic.Hooks (JSX, ReactComponent, element) --- | Create a React error boundary with the given name. The resulting --- | component takes a render callback which exposes the error if one --- | exists and an effect for dismissing the error. +--| Create a React error boundary with the given name. The resulting +--| component takes a render callback which exposes the error if one +--| exists and an effect for dismissing the error. mkErrorBoundary :: String -> Effect diff --git a/src/React/Basic/Hooks/Internal.purs b/src/React/Basic/Hooks/Internal.purs index 27cfaa8..b3f30aa 100644 --- a/src/React/Basic/Hooks/Internal.purs +++ b/src/React/Basic/Hooks/Internal.purs @@ -23,56 +23,56 @@ import Effect (Effect) import Prelude (bind) as Prelude import Type.Equality (class TypeEquals) --- | Render represents the effects allowed within a React component's --- | body, i.e. during "render". This includes hooks and ends with --- | returning JSX (see `pure`), but does not allow arbitrary side --- | effects. --- | --- | The `x` and `y` type arguments represent the stack of effects that this --- | `Render` implements, with `x` being the stack at the start of this --- | `Render`, and `y` the stack at the end. --- | --- | See --- | [purescript-indexed-monad](https://pursuit.purescript.org/packages/purescript-indexed-monad) --- | to understand how the order of the stack is enforced at the type level. +--| Render represents the effects allowed within a React component's +--| body, i.e. during "render". This includes hooks and ends with +--| returning JSX (see `pure`), but does not allow arbitrary side +--| effects. +--| +--| The `x` and `y` type arguments represent the stack of effects that this +--| `Render` implements, with `x` being the stack at the start of this +--| `Render`, and `y` the stack at the end. +--| +--| See +--| [purescript-indexed-monad](https://pursuit.purescript.org/packages/purescript-indexed-monad) +--| to understand how the order of the stack is enforced at the type level. newtype Render :: Type -> Type -> Type -> Type newtype Render x y a = Render (Effect a) --- | Rename/alias a chain of hooks. Useful for exposing a single --- | "clean" type when creating a hook to improve error messages --- | and hide implementation details, particularly for libraries --- | hiding internal info. --- | --- | For example, the following alias is technically correct but --- | when inspecting types or error messages the alias is expanded --- | to the full original type and `UseAff` is never seen: --- | --- | ```purs --- | type UseAff deps a hooks --- | = UseEffect deps (UseState (Result a) hooks) --- | --- | useAff :: ... -> Hook (UseAff deps a) (Result a) --- | useAff deps aff = React.do --- | ... --- | ``` --- | --- | `coerceHook` allows the same code to safely export a newtype --- | instead, hiding the internal implementation: --- | --- | ```purs --- | newtype UseAff deps a hooks --- | = UseAff (UseEffect deps (UseState (Result a) hooks)) --- | --- | derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ --- | --- | useAff :: ... -> Hook (UseAff deps a) (Result a) --- | useAff deps aff = coerceHook React.do --- | ... --- | ``` --- | --- | --- | +--| Rename/alias a chain of hooks. Useful for exposing a single +--| "clean" type when creating a hook to improve error messages +--| and hide implementation details, particularly for libraries +--| hiding internal info. +--| +--| For example, the following alias is technically correct but +--| when inspecting types or error messages the alias is expanded +--| to the full original type and `UseAff` is never seen: +--| +--| ```purs +--| type UseAff deps a hooks +--| = UseEffect deps (UseState (Result a) hooks) +--| +--| useAff :: ... -> Hook (UseAff deps a) (Result a) +--| useAff deps aff = React.do +--| ... +--| ``` +--| +--| `coerceHook` allows the same code to safely export a newtype +--| instead, hiding the internal implementation: +--| +--| ```purs +--| newtype UseAff deps a hooks +--| = UseAff (UseEffect deps (UseState (Result a) hooks)) +--| +--| derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ +--| +--| useAff :: ... -> Hook (UseAff deps a) (Result a) +--| useAff deps aff = coerceHook React.do +--| ... +--| ``` +--| +--| +--| coerceHook :: forall hooks oldHook newHook a. Newtype newHook oldHook => @@ -80,64 +80,64 @@ coerceHook :: Render hooks newHook a coerceHook (Render a) = Render a --- | Promote an arbitrary Effect to a Hook. --- | --- | This is unsafe because it allows arbitrary --- | effects to be performed during a render, which --- | may cause them to be run many times by React. --- | This function is primarily for constructing --- | new hooks using the FFI. If you just want to --- | alias a safe hook's effects, prefer `coerceHook`. --- | --- | It's also unsafe because the author of the hook --- | type (the `newHook` type variable used here) _MUST_ --- | contain all relevant types. For example, `UseState` --- | has a phantom type to track the type of the value contained. --- | `useEffect` tracks the type used as the deps. `useAff` tracks --- | both the deps and the resulting response's type. Forgetting --- | to do this allows the consumer to reorder hook effects. If --- | `useState` didn't track the return type the following --- | extremely unsafe code would be allowed: --- | --- | ```purs --- | React.do --- | if xyz then --- | _ <- useState 0 --- | useState Nothing --- | else --- | s <- useState Nothing --- | _ <- useState 0 --- | pure s --- | ... --- | ``` --- | --- | The same applies to `deps` in these examples as they use --- | `Eq` and a reorder would allow React to pass incorrect --- | types into the `eq` function! +--| Promote an arbitrary Effect to a Hook. +--| +--| This is unsafe because it allows arbitrary +--| effects to be performed during a render, which +--| may cause them to be run many times by React. +--| This function is primarily for constructing +--| new hooks using the FFI. If you just want to +--| alias a safe hook's effects, prefer `coerceHook`. +--| +--| It's also unsafe because the author of the hook +--| type (the `newHook` type variable used here) _MUST_ +--| contain all relevant types. For example, `UseState` +--| has a phantom type to track the type of the value contained. +--| `useEffect` tracks the type used as the deps. `useAff` tracks +--| both the deps and the resulting response's type. Forgetting +--| to do this allows the consumer to reorder hook effects. If +--| `useState` didn't track the return type the following +--| extremely unsafe code would be allowed: +--| +--| ```purs +--| React.do +--| if xyz then +--| _ <- useState 0 +--| useState Nothing +--| else +--| s <- useState Nothing +--| _ <- useState 0 +--| pure s +--| ... +--| ``` +--| +--| The same applies to `deps` in these examples as they use +--| `Eq` and a reorder would allow React to pass incorrect +--| types into the `eq` function! unsafeHook :: forall newHook a. Effect a -> Hook newHook a unsafeHook = Render --- | Promote an arbitrary Effect to a Pure render effect. --- | --- | This is unsafe because it allows arbitrary --- | effects to be performed during a render, which --- | may cause them to be run many times by React. --- | You should almost always prefer `useEffect`! +--| Promote an arbitrary Effect to a Pure render effect. +--| +--| This is unsafe because it allows arbitrary +--| effects to be performed during a render, which +--| may cause them to be run many times by React. +--| You should almost always prefer `useEffect`! unsafeRenderEffect :: forall a. Effect a -> Pure a unsafeRenderEffect = Render --- | Type alias used to lift otherwise pure functionality into the Render type. --- | Not commonly used. +--| Type alias used to lift otherwise pure functionality into the Render type. +--| Not commonly used. type Pure a = forall hooks. Render hooks hooks a --- | Type alias for Render representing a hook. --- | --- | The `newHook` argument is a type constructor which takes a set of existing --- | effects and generates a type with a new set of effects (produced by this --- | hook) stacked on top. +--| Type alias for Render representing a hook. +--| +--| The `newHook` argument is a type constructor which takes a set of existing +--| effects and generates a type with a new set of effects (produced by this +--| hook) stacked on top. type Hook (newHook :: Type -> Type) a = forall hooks. Render hooks (newHook hooks) a @@ -155,11 +155,11 @@ instance ixBindRender :: IxBind Render where instance ixMonadRender :: IxMonad Render --- | Exported for use with qualified-do syntax +--| Exported for use with qualified-do syntax bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b bind = ibind --- | Exported for use with qualified-do syntax +--| Exported for use with qualified-do syntax discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b discard = ibind @@ -186,11 +186,11 @@ instance monoidRender :: (TypeEquals x y, Monoid a) => Monoid (Render x y a) whe type HookApply hooks (newHook :: Type -> Type) = newHook hooks --- | Applies a new hook to a hook chain, with the innermost hook as the left argument. --- | This allows hook chains to be written in reverse order, aligning them with the --- | order they appear when actually used in do-notation. --- | ```purescript --- | type UseCustomHook hooks = UseEffect String (UseState Int hooks) --- | type UseCustomHook' = UseState Int & UseEffect String --- | ``` +--| Applies a new hook to a hook chain, with the innermost hook as the left argument. +--| This allows hook chains to be written in reverse order, aligning them with the +--| order they appear when actually used in do-notation. +--| ```purescript +--| type UseCustomHook hooks = UseEffect String (UseState Int hooks) +--| type UseCustomHook' = UseState Int & UseEffect String +--| ``` infixl 0 type HookApply as & \ No newline at end of file diff --git a/src/React/Basic/Hooks/ResetToken.purs b/src/React/Basic/Hooks/ResetToken.purs index f487181..685e9c4 100644 --- a/src/React/Basic/Hooks/ResetToken.purs +++ b/src/React/Basic/Hooks/ResetToken.purs @@ -11,15 +11,15 @@ import Effect (Effect) import React.Basic.Hooks (type (/\), Hook, UseState, coerceHook, useState, (/\)) import React.Basic.Hooks as React --- | Useful for resetting effects or component state. A `ResetToken` can be --- | used alongside other hook dependencies to force a reevaluation of --- | whatever depends on those dependencies. --- | --- | For example, consider an effect or API call which depends on the state --- | of a search bar. You may want a button in the UI to refresh stale data. --- | In this case you would include a `ResetToken` in your search effect/aff's --- | dependencies and call `useResetToken`'s reset effect in the button's --- | `onClick` handler. +--| Useful for resetting effects or component state. A `ResetToken` can be +--| used alongside other hook dependencies to force a reevaluation of +--| whatever depends on those dependencies. +--| +--| For example, consider an effect or API call which depends on the state +--| of a search bar. You may want a button in the UI to refresh stale data. +--| In this case you would include a `ResetToken` in your search effect/aff's +--| dependencies and call `useResetToken`'s reset effect in the button's +--| `onClick` handler. useResetToken :: Hook UseResetToken (ResetToken /\ (Effect Unit)) useResetToken = coerceHook React.do diff --git a/src/React/Basic/Hooks/Suspense.purs b/src/React/Basic/Hooks/Suspense.purs index 4d49aaf..cfe068d 100644 --- a/src/React/Basic/Hooks/Suspense.purs +++ b/src/React/Basic/Hooks/Suspense.purs @@ -13,20 +13,20 @@ import Effect.Aff (Error, Fiber, joinFiber, throwError) import React.Basic.Hooks (JSX, Pure, ReactComponent, element, unsafeRenderEffect) import Unsafe.Coerce (unsafeCoerce) --- | Suspend rendering until a result exists. --- | --- | *Note: Error and loading states are thrown to React! Don't forget --- | to implement a React error boundary and ensure `suspend` is --- | only called from a child of at least one `suspense` parent!* --- | --- | *Note: You probably shouldn't be using this function directly. It's --- | primarily for library authors to build abstractions on top of, as --- | it requires things like caching mechanisms external to the --- | component tree.* --- | --- | *Warning: React's Suspense API is still experimental. It requires --- | some manual setup as well as specific versions of React. The API --- | is also not final and these functions may change.* +--| Suspend rendering until a result exists. +--| +--| *Note: Error and loading states are thrown to React! Don't forget +--| to implement a React error boundary and ensure `suspend` is +--| only called from a child of at least one `suspense` parent!* +--| +--| *Note: You probably shouldn't be using this function directly. It's +--| primarily for library authors to build abstractions on top of, as +--| it requires things like caching mechanisms external to the +--| component tree.* +--| +--| *Warning: React's Suspense API is still experimental. It requires +--| some manual setup as well as specific versions of React. The API +--| is also not final and these functions may change.* suspend :: forall a. Suspended a -> Pure a suspend (Suspended e) = React.do unsafeRenderEffect do @@ -51,8 +51,8 @@ suspense = element suspense_ foreign import suspense_ :: ReactComponent { children :: Array JSX, fallback :: JSX } --- | Dangerously throw a `Promise` as though it were an `Error`. --- | React's Suspense API catches thrown `Promise`s and suspends --- | rendering until they complete. +--| Dangerously throw a `Promise` as though it were an `Error`. +--| React's Suspense API catches thrown `Promise`s and suspends +--| rendering until they complete. unsafeThrowPromise :: forall a. Promise a -> Effect a unsafeThrowPromise = throwError <<< (unsafeCoerce :: Promise a -> Error) diff --git a/src/React/Basic/Hooks/Suspense/Store.purs b/src/React/Basic/Hooks/Suspense/Store.purs index e24f6dd..0c38260 100644 --- a/src/React/Basic/Hooks/Suspense/Store.purs +++ b/src/React/Basic/Hooks/Suspense/Store.purs @@ -28,7 +28,7 @@ import React.Basic.Hooks.Suspense (Suspended(..), SuspenseResult(..)) import Web.HTML (window) import Web.HTML.Window (requestIdleCallback) --- | Simple key-based cache. +--| Simple key-based cache. mkSuspenseStore :: forall k v. Ord k => diff --git a/test/Spec/MemoSpec.purs b/test/Spec/MemoSpec.purs new file mode 100644 index 0000000..5c3741c --- /dev/null +++ b/test/Spec/MemoSpec.purs @@ -0,0 +1,106 @@ +module Test.Spec.MemoSpec where + +import Prelude + +import Data.Function (on) +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) +import Effect.Ref (modify, new, read) +import React.Basic.DOM as R +import React.Basic.Hooks (ReactComponent, element, memo, memo', reactComponent, useEffectAlways) +import React.Basic.Hooks as Hooks +import React.TestingLibrary (cleanup, render) +import Test.Spec (Spec, after_, before, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec ∷ Spec Unit +spec = + after_ cleanup do + before setup do + describe "memo" do + it "works with simple values" \{ memoTest } -> do + rendersRef <- liftEffect do new 0 + let onRender = void $ modify (1 + _) rendersRef + let renders = liftEffect do read rendersRef + { rerender } <- render do element memoTest { onRender, arg: 0 } + rerender do element memoTest { onRender, arg: 0 } + renders >>= (_ `shouldEqual` 1) + rerender do element memoTest { onRender, arg: 1 } + renders >>= (_ `shouldEqual` 2) + + describe "memo'" do + it "never renders if the eq fn returns true" \{ memo'TestAlwaysEq } -> do + rendersRef <- liftEffect do new 0 + let onRender = void $ modify (1 + _) rendersRef + let renders = liftEffect do read rendersRef + { rerender } <- render do element memo'TestAlwaysEq { onRender, arg: 0 } + rerender do element memo'TestAlwaysEq { onRender, arg: 0 } + renders >>= (_ `shouldEqual` 1) + rerender do element memo'TestAlwaysEq { onRender, arg: 1 } + renders >>= (_ `shouldEqual` 1) + + it "always renders if the eq fn returns false" \{ memo'TestNeverEq } -> do + rendersRef <- liftEffect do new 0 + let onRender = void $ modify (1 + _) rendersRef + let renders = liftEffect do read rendersRef + { rerender } <- render do element memo'TestNeverEq { onRender, arg: 0 } + rerender do element memo'TestNeverEq { onRender, arg: 0 } + renders >>= (_ `shouldEqual` 2) + rerender do element memo'TestNeverEq { onRender, arg: 1 } + renders >>= (_ `shouldEqual` 3) + + it "renders correctly over eq on props.arg" \{ memo'TestArgEq } -> do + rendersRef <- liftEffect do new 0 + let onRender = void $ modify (1 + _) rendersRef + let renders = liftEffect do read rendersRef + { rerender } <- render do element memo'TestArgEq { onRender, arg: 0 } + rerender do element memo'TestArgEq { onRender, arg: 0 } + renders >>= (_ `shouldEqual` 1) + rerender do element memo'TestArgEq { onRender, arg: 1 } + renders >>= (_ `shouldEqual` 2) + rerender do element memo'TestArgEq { onRender, arg: 1 } + renders >>= (_ `shouldEqual` 2) + where + setup :: + forall m. MonadEffect m => + m + { memoTest :: ReactComponent { onRender :: Effect Unit, arg :: Int } + , memo'TestAlwaysEq :: ReactComponent { onRender :: Effect Unit, arg :: Int } + , memo'TestNeverEq :: ReactComponent { onRender :: Effect Unit, arg :: Int } + , memo'TestArgEq :: ReactComponent { onRender :: Effect Unit, arg :: Int } + } + setup = liftEffect do + memoTest <- memo do + reactComponent "MemoTest" \{ onRender } -> Hooks.do + useEffectAlways do + onRender + pure (pure unit) + pure $ R.div_ [] + + memo'TestAlwaysEq <- memo' (\_ _ -> true) do + reactComponent "MemoTest" \{ onRender } -> Hooks.do + useEffectAlways do + onRender + pure (pure unit) + pure $ R.div_ [] + + memo'TestNeverEq <- memo' (\_ _ -> false) do + reactComponent "MemoTest" \{ onRender } -> Hooks.do + useEffectAlways do + onRender + pure (pure unit) + pure $ R.div_ [] + + memo'TestArgEq <- memo' (eq `on` _.arg) do + reactComponent "MemoTest" \{ onRender } -> Hooks.do + useEffectAlways do + onRender + pure (pure unit) + pure $ R.div_ [] + + pure + { memoTest + , memo'TestAlwaysEq + , memo'TestNeverEq + , memo'TestArgEq + } \ No newline at end of file