From 90893fe8e4629392fe22c8575f6eec74f60b1c85 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sat, 19 Sep 2020 14:26:33 +0200 Subject: [PATCH 01/29] Upgrade to reason-react 9.1 --- package.json | 2 +- yarn.lock | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 7be4cc7cf..166e1a372 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "reason-promise": "^1.0.2", - "reason-react": "^0.7.0", + "reason-react": "^0.9.1", "remark-parse": "^7.0.1", "remark-slug": "^5.1.2", "remark-stringify": "^7.0.3", diff --git a/yarn.lock b/yarn.lock index a46dca976..050bc4672 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5809,7 +5809,7 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -react-dom@>=16.8.1, react-dom@^16.12.0: +react-dom@^16.12.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== @@ -5829,7 +5829,7 @@ react-refresh@0.8.3: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== -react@>=16.8.1, react@^16.12.0: +react@^16.12.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== @@ -5893,13 +5893,10 @@ reason-promise@^1.0.2: resolved "https://registry.yarnpkg.com/reason-promise/-/reason-promise-1.1.1.tgz#966133fed21e748a50ffb8839a1da04912bcf380" integrity sha512-xMXDiyzTjn7t9pq9aQrkgu8CLB5DILU70oWQ+LI20YecshypUwsw37TKOvCxgoFR3vo30ucF0/iWe2BZ181/jw== -reason-react@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/reason-react/-/reason-react-0.7.1.tgz#e6acea88542cd44398cd980093b8a2ab2722744e" - integrity sha512-Ssx4jZYohMHW9ZiW893IfbYdZw/muSmPFKigAgL+AORUgyiaphb0PP4yRGlx9A7JAxR3EeBn294XKUaClJQhbA== - dependencies: - react ">=16.8.1" - react-dom ">=16.8.1" +reason-react@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/reason-react/-/reason-react-0.9.1.tgz#30a887158200b659aa03e2d75ff4cc54dc462bb0" + integrity sha512-nlH0O2TDy9KzOLOW+vlEQk4ExHOeciyzFdoLcsmmiit6hx6H5+CVDrwJ+8aiaLT/kqK5xFOjy4PS7PftWz4plA== reduce-css-calc@^2.1.6: version "2.1.7" From e64dbf3ce41b12de5468825cfa348cafdf9b8a59 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sat, 19 Sep 2020 14:32:38 +0200 Subject: [PATCH 02/29] Add new layout for the React docs, prepare work for more generic DocsLayout --- common/App.re | 13 +++- components/VersionSelect.re | 2 +- layouts/CommunityLayout.re | 2 +- layouts/DocsLayout.re | 110 +++++++++++++++++++++++++++++-- layouts/ManualDocsLayout.re | 1 + layouts/ManualDocsLayout8_0_0.re | 1 + layouts/ReactDocsLayout.re | 69 +++++++++++++++++++ layouts/SidebarLayout.re | 14 +++- scripts/extract-tocs.js | 50 +++++++++++--- 9 files changed, 242 insertions(+), 20 deletions(-) create mode 100644 layouts/ReactDocsLayout.re diff --git a/common/App.re b/common/App.re index cc3645978..4ee0d9df2 100644 --- a/common/App.re +++ b/common/App.re @@ -110,6 +110,10 @@ let default = (props: props): React.element => { } | {base: [|"docs", "reason-compiler"|], version: Latest} => content + | {base: [|"docs", "react"|], version: Latest} => + frontmatter}> + content + | {base: [|"docs", "gentype"|], version: Latest} => content // common routes @@ -123,12 +127,17 @@ let default = (props: props): React.element => { // to keep the frontmatter parsing etc in one place content | _ => + let title = + switch (url) { + | {base: [|"docs"|]} => Some("Overview | ReScript Documentation") + | _ => None + }; - +
content
-
+ ; } }; }; diff --git a/components/VersionSelect.re b/components/VersionSelect.re index 1f148331d..f96334c78 100644 --- a/components/VersionSelect.re +++ b/components/VersionSelect.re @@ -17,7 +17,7 @@ let make = }, ); + +}; +``` + +### Passing `setState` to a Child Component + +In this example, we are creating a `ThemeContainer` component that manages a `darkmode` boolean state and passes the `setDarkmode` function to a `ControlPanel` component to trigger the state changes. + + + + +```res +// ThemeContainer.res +module ControlPanel = { + @react.component + let make = (~setDarkmode, ~darkmode) => { + let onClick = evt => { + ReactEvent.Mouse.preventDefault(evt) + setDarkmode(prev => !prev) + } + + let toggleText = "Switch to " ++ ((darkmode ? "light" : "dark") ++ " theme") + +
+ } +} + +@react.component +let make = (~content) => { + let (darkmode, setDarkmode) = React.useState(_ => false) + + let className = darkmode ? "theme-dark" : "theme-light" + +
+
+

{React.string("More Infos about ReScript")}

content +
+ +
+} +``` +```js +function ControlPanel(Props) { + var setDarkmode = Props.setDarkmode; + var darkmode = Props.darkmode; + var onClick = function (evt) { + evt.preventDefault(); + return Curry._1(setDarkmode, (function (prev) { + return !prev; + })); + }; + var toggleText = "Switch to " + (( + darkmode ? "light" : "dark" + ) + " theme"); + return React.createElement("div", undefined, React.createElement("button", { + onClick: onClick + }, toggleText)); +} + +function ThemeContainer(Props) { + var content = Props.content; + var match = React.useState(function () { + return false; + }); + var darkmode = match[0]; + var className = darkmode ? "theme-dark" : "theme-light"; + return React.createElement("div", { + className: className + }, React.createElement("section", undefined, React.createElement("h1", undefined, "More Infos about ReScript"), content), React.createElement(Playground$ControlPanel, { + setDarkmode: match[1], + darkmode: darkmode + })); +} +``` + +
+ +Note that whenever `setDarkmode` is returning a new value (e.g. switching from `true` -> `false`), it will cause a re-render for `ThemeContainer`'s `className` and the toggle text of its nested `ControlPanel`. + + +## Uncurried Version + +For cleaner JS output, you can use `React.Uncurried.useState` instead: + + + +```res +let (state, setState) = React.Uncurried.useState(_ => 0) + +setState(. prev => prev + 1) +``` + +```js +var match = React.useState(function () { + return 0; + }); + +var setState = match[1]; + +setState(function (prev) { + return prev + 1 | 0; + }); +``` + + + diff --git a/scripts/extract-tocs.js b/scripts/extract-tocs.js index 78325bb88..44a37330b 100644 --- a/scripts/extract-tocs.js +++ b/scripts/extract-tocs.js @@ -154,6 +154,10 @@ const createReactToc = () => { "rendering-elements", "components-and-props", "arrays-and-keys", + "hooks-overview", + "hooks-state", + "hooks-effect", + "hooks-context", ]; const files = glob.sync(`${MD_DIR}/*.md?(x)`); From fe901b1ba05d7c65115e50ffa9bc156310eac880 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Tue, 22 Sep 2020 23:23:36 +0200 Subject: [PATCH 11/29] Update rendering elemnts, hooks-overview; add hooks-context --- pages/docs/react/latest/hooks-context.mdx | 151 ++++++++++++++++++ pages/docs/react/latest/hooks-overview.mdx | 2 - .../docs/react/latest/rendering-elements.mdx | 7 +- 3 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 pages/docs/react/latest/hooks-context.mdx diff --git a/pages/docs/react/latest/hooks-context.mdx b/pages/docs/react/latest/hooks-context.mdx new file mode 100644 index 000000000..ef67659e2 --- /dev/null +++ b/pages/docs/react/latest/hooks-context.mdx @@ -0,0 +1,151 @@ +--- +title: useContext Hook +description: "Details about the useContext React hook in ReScript" +canonical: "/docs/react/latest/hooks-context" +category: "Hooks & State Management" +--- + +# useContext + + + +Context provides a way to pass data through the component tree without having to pass props down manually at every level. The `useContext` hooks gives access to such Context values. + + + +## Usage + +```res +let value = React.useContext(myContext) +``` + +Accepts a `React.Context.t` (the value returned from `React.createContext`) and returns the current context value for that context. The current context value is determined by the value prop of the nearest `` above the calling component in the tree. + + +## Examples + +### A Simple ThemeProvider + + + + +```res +// App.re +module ThemeContext = { + let context = React.createContext("light") + + module Provider = { + let provider = React.Context.provider(context) + + @react.component + let make = (~value, ~children) => { + React.createElement(provider, {"value": value, "children": children}) + } + } +} + +module ThemedButton = { + @react.component + let make = () => { + let theme = React.useContext(ThemeContext.context) + let (color, backgroundColor) = switch theme { + | "dark" => ("#ffffff", "#222222") + | "light" | _ => ("#000000", "#eeeeee") + } + + let style = ReactDOMStyle.make(~color, ~backgroundColor, ()) + + + } +} + +module Toolbar = { + @react.component + let make = () => { +
+ } +} + +@react.component +let make = () => { + +
+
+} +``` +```js +var context = React.createContext("light"); + +var provider = context.Provider; + +function ThemeContext$Provider(Props) { + var value = Props.value; + var children = Props.children; + return React.createElement(provider, { + value: value, + children: children + }); +} + +var Provider = { + provider: provider, + make: ThemeContext$Provider +}; + +var ThemeContext = { + context: context, + Provider: Provider +}; + +function ThemedButton(Props) { + var theme = React.useContext(context); + var match; + switch (theme) { + case "dark" : + match = [ + "#ffffff", + "#222222" + ]; + break; + case "light" : + match = [ + "#000000", + "#eeeeee" + ]; + break; + default: + match = [ + "#000000", + "#eeeeee" + ]; + } + var style = { + backgroundColor: match[1], + color: match[0] + }; + return React.createElement("button", { + style: style + }, "I am a styled button!"); +} + +var ThemedButton = { + make: ThemedButton +}; + +function Toolbar(Props) { + return React.createElement("div", undefined, React.createElement(ThemedButton, {})); +} + +var Toolbar = { + make: Toolbar +}; + +function App(Props) { + return React.createElement(ThemeContext$Provider, { + value: "dark", + children: React.createElement("div", undefined, React.createElement(Toolbar, {})) + }); +} +``` + +
diff --git a/pages/docs/react/latest/hooks-overview.mdx b/pages/docs/react/latest/hooks-overview.mdx index f5d785590..e04f9e1bb 100644 --- a/pages/docs/react/latest/hooks-overview.mdx +++ b/pages/docs/react/latest/hooks-overview.mdx @@ -28,7 +28,6 @@ Just for a quick look, here is an example of a `Counter` component that allows a ```res - // Counter.re @react.component let make = () => { @@ -45,7 +44,6 @@ let make = () => { } - ``` ```js function Counter(Props) { diff --git a/pages/docs/react/latest/rendering-elements.mdx b/pages/docs/react/latest/rendering-elements.mdx index 30501d722..98e0d08d4 100644 --- a/pages/docs/react/latest/rendering-elements.mdx +++ b/pages/docs/react/latest/rendering-elements.mdx @@ -123,6 +123,11 @@ React.createElement("div", undefined, element); +### Creating Variadic Elements + + + + ## Cloning Elements **Note:** This is an escape hatch feature and will only be useful for interoping with existing JS code / libraries. @@ -181,7 +186,7 @@ First you'd need to find a DOM node to mount your app to. For this, you can use // Dom access can actually fail. ResScript // is really explicit about handling edge cases! switch(ReactDOM.querySelector("#root")){ - | Some(root) => ReactDOM.render(root,
React.string("Hello Andrea")
) + | Some(root) => ReactDOM.render(
React.string("Hello Andrea")
, root) | None => () // do nothing } ``` From 6bc71ab692ff4108df7257f429b197d4bec88eba Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Mon, 28 Sep 2020 19:13:08 +0200 Subject: [PATCH 12/29] Add refs / forwardRefs / useRef docs --- pages/docs/react/latest/forwarding-refs.mdx | 136 +++++++++ pages/docs/react/latest/hooks-ref.mdx | 152 ++++++++++ pages/docs/react/latest/refs-and-the-dom.mdx | 300 +++++++++++++++++++ scripts/extract-tocs.js | 2 + 4 files changed, 590 insertions(+) create mode 100644 pages/docs/react/latest/forwarding-refs.mdx create mode 100644 pages/docs/react/latest/hooks-ref.mdx create mode 100644 pages/docs/react/latest/refs-and-the-dom.mdx diff --git a/pages/docs/react/latest/forwarding-refs.mdx b/pages/docs/react/latest/forwarding-refs.mdx new file mode 100644 index 000000000..8137e7a8f --- /dev/null +++ b/pages/docs/react/latest/forwarding-refs.mdx @@ -0,0 +1,136 @@ +--- +title: Forwarding Refs +description: "Forwarding Ref values in ReScript and React" +canonical: "/docs/react/latest/forwarding-refs" +category: "Guides" +--- + +# Forwarding Refs + + + +Ref forwarding is a technique for automatically passing a [React.ref](./refs-and-the-dom) through a component to one of its children. This is typically not necessary for most components in the application. However, it can be useful for some kinds of components, especially in reusable component libraries. The most common scenarios are described below. + + + +## Forwarding Refs to DOM Components + +Consider a FancyButton component that renders the native button DOM element: + +```res +// FancyButton.res + +@react.component +let make = (~children) => { + +} +``` + +React components hide their implementation details, including their rendered output. Other components using FancyButton **usually will not need** to obtain a ref to the inner button DOM element. This is good because it prevents components from relying on each other’s DOM structure too much. + +Although such encapsulation is desirable for application-level components like `FeedStory` or `Comment`, it can be inconvenient for highly reusable “leaf” components like `FancyButton` or `MyTextInput`. These components tend to be used throughout the application in a similar manner as a regular DOM button and input, and accessing their DOM nodes may be unavoidable for managing focus, selection, or animations. + +**Ref forwarding is an opt-in feature that lets some components take a ref they receive, and pass it further down (in other words, “forward” it) to a child.** + +In the example below, `FancyInput` uses `React.forwardRef` to obtain the ref passed to it, and then forward it to the DOM input that it renders: + + + + +```res +// App.res + +module FancyInput = { + @react.component + let make = React.forwardRef((~className=?, ~children, ref_) => +
+ Belt.Option.map( + ReactDOMRe.Ref.domRef, + )} + /> + children +
+ ) +} + +@bs.send external focus: Dom.element => unit = "focus" + +@react.component +let make = () => { + let input = React.useRef(Js.Nullable.null) + + let focusInput = () => + input.current + ->Js.Nullable.toOption + ->Belt.Option.forEach(input => input->focus) + + let onClick = _ => focusInput() + +
+ + + +
+} +``` + +```js +var React = require("react"); +var Belt_Option = require("./stdlib/belt_Option.js"); +var Caml_option = require("./stdlib/caml_option.js"); + +var make = React.forwardRef(function (Props, ref_) { + var className = Props.className; + var children = Props.children; + var tmp = { + type: "text" + }; + var tmp$1 = Belt_Option.map((ref_ == null) ? undefined : Caml_option.some(ref_), (function (prim) { + return prim; + })); + if (tmp$1 !== undefined) { + tmp.ref = Caml_option.valFromOption(tmp$1); + } + if (className !== undefined) { + tmp.className = Caml_option.valFromOption(className); + } + return React.createElement("div", undefined, React.createElement("input", tmp), children); + }); + +var FancyInput = { + make: make +}; + +function App(Props) { + var input = React.useRef(null); + var onClick = function (param) { + return Belt_Option.forEach(Caml_option.nullable_to_opt(input.current), (function (input) { + input.focus(); + + })); + }; + return React.createElement("div", undefined, React.createElement(make, { + className: "fancy", + children: React.createElement("button", { + onClick: onClick + }, "Click to focus"), + ref: input + })); +} +``` + +
+ +**Note:** Our `@react.component` decorator transforms our labeled argument props within our `React.forwardRef` function in the same manner as our classic `make` function. + +This way, components using `FancyInput` can get a ref to the underlying `input` DOM node and access it if necessary—just like if they used a DOM `input` directly. + +## Note for Component Library Maintainers + + +**When you start using forwardRef in a component library, you should treat it as a breaking change and release a new major version of your library**. This is because your library likely has an observably different behavior (such as what refs get assigned to, and what types are exported), and this can break apps and other libraries that depend on the old behavior. diff --git a/pages/docs/react/latest/hooks-ref.mdx b/pages/docs/react/latest/hooks-ref.mdx new file mode 100644 index 000000000..c6c180a16 --- /dev/null +++ b/pages/docs/react/latest/hooks-ref.mdx @@ -0,0 +1,152 @@ +--- +title: useRef Hook +description: "Details about the useRef React hook in ReScript" +canonical: "/docs/react/latest/hooks-ref" +category: "Hooks & State Management" +--- + +# useRef + + + +The `useRef` hooks creates and manages mutable containers inside your React component. + + + +## Usage + + + +```res +let refContainer = React.useRef(initialValue); +``` + +```js + var button = React.useRef(null); + React.useRef(0); +``` + + + +`React.useRef` returns a mutable ref object whose `.current` record field is initialized to the passed argument (`initialValue`). The returned object will persist for the full lifetime of the component. + +Essentially, a `React.ref` is like a "box" that can hold a mutable value in its `.current` record field. + +You might be familiar with refs primarily as a way to access the DOM. If you pass a ref object to React with `
`, React will set its `.current` property to the corresponding DOM node whenever that node changes. + +However, `useRef()` is useful for more than the ref attribute. It's handy for keeping any mutable value around similar to how you’d use instance fields in classes. + +This works because `useRef()` creates a plain JavaScript object. The only difference between `useRef()` and creating a `{current: ...}` object yourself is that useRef will give you the same ref object on every render. + + +Keep in mind that `useRef` doesn’t notify you when its content changes. Mutating the `.current` record field doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a [callback ref](./refs-and-the-dom#callback-refs) instead. + +More infos on direct DOM manipulation can be found in the [Refs and the DOM](./refs-and-the-dom) section. + +## Examples + +### Managing Focus for a Text Input + + + + +```res +// TextInputWithFocusButton.re + +@bs.send external focus: Dom.element => unit = "focus" + +@react.component +let make = () => { + let inputEl = React.useRef(Js.Nullable.null) + + let onClick = _ => { + inputEl.current + ->Js.Nullable.toOption + ->Belt.Option.forEach(input => input->focus) + } + + <> + + + +} +``` + +```js +function TextInputWithFocusButton(Props) { + var inputEl = React.useRef(null); + var onClick = function (param) { + return Belt_Option.forEach(Caml_option.nullable_to_opt(inputEl.current), (function (input) { + input.focus(); + + })); + }; + return React.createElement(React.Fragment, undefined, React.createElement("input", { + ref: inputEl, + type: "text" + }), React.createElement("button", { + onClick: onClick + }, "Focus the input")); +} +``` + + + +### Using a Callback Ref + +Reusing the example from our [Refs and the DOM](./refs-and-the-dom#callback-refs) section: + + + +```res +// CustomTextInput.re + +@bs.send external focus: Dom.element => unit = "focus" + +@react.component +let make = () => { + let textInput = React.useRef(Js.Nullable.null) + let setTextInputRef = element => { + textInput.current = element; + } + + let focusTextInput = _ => { + textInput.current + ->Js.Nullable.toOption + ->Belt.Option.forEach(input => input->focus) + } + +
+ + +
+} +``` + +```js +function CustomTextInput(Props) { + var textInput = React.useRef(null); + var setTextInputRef = function (element) { + textInput.current = element; + + }; + var focusTextInput = function (param) { + return Belt_Option.forEach(Caml_option.nullable_to_opt(textInput.current), (function (input) { + input.focus(); + + })); + }; + return React.createElement("div", undefined, React.createElement("input", { + ref: setTextInputRef, + type: "text" + }), React.createElement("input", { + type: "button", + value: "Focus the text input", + onClick: focusTextInput + })); +} +``` + +
diff --git a/pages/docs/react/latest/refs-and-the-dom.mdx b/pages/docs/react/latest/refs-and-the-dom.mdx new file mode 100644 index 000000000..93714c646 --- /dev/null +++ b/pages/docs/react/latest/refs-and-the-dom.mdx @@ -0,0 +1,300 @@ +--- +title: Refs and the DOM +description: "Using Refs and DOM elements in ReScript and React" +canonical: "/docs/react/latest/refs-and-the-dom" +category: "Main Concepts" +--- + +# Refs and the DOM + + + +Refs provide a way to access DOM nodes or React elements created within your `make` component function. + + + + +In the typical React dataflow, [props](./components-and-props) are the only way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are a few cases where you need to imperatively modify a child outside of the typical dataflow. The child to be modified could be an `React.element`, or it could be a `Dom.element`. For both of these cases, React provides an escape hatch. + +A `React.ref` is defined like this: + +```res +type t<'value> = { mutable current: 'value } +``` + +> *Note that the `Ref.ref` should not to be confused with the builtin [ref type](/docs/manual/latest/mutation), the language feature that enables mutation.* + +## When to use Refs + +There are a few good use cases for refs: + +- Managing focus, text selection, or media playback. +- Triggering imperative animations. +- Integrating with third-party DOM libraries. + +Avoid using refs for anything that can be done declaratively. + +## Creating Refs + +A React ref is represented as a `React.ref('value)` type, a container managing a mutable value of type `'value`. You can create this kind of ref with the [React.useRef](./hooks-ref) hook: + +```res +@react.component +let make = () => { + let clicks = React.useRef(0); + + let onClick = (_) => { + clicks.current = clicks.current + 1; + }; + +
+ {Belt.Int.toString(clicks.current)->React.string} +
+} +``` + +The example above defines a binding `clicks` of type `React.ref(int)`. Note how changing the value `clicks.current` doesn't trigger any re-rendering of the component. + +## Accessing Refs + +When a ref is passed to an element during render, a reference to the node becomes accessible at the current attribute of the ref. + +```res +let value = myRef.current +``` + +The value of the ref differs depending on the type of the node: +- When the ref attribute is used on an HTML element, the ref passed via `ReactDOM.Ref.domRef` receives the underlying DOM element as its current property (type of `React.ref>`) +- In case of interop, when the ref attribute is used on a custom class component (based on JS classes), the ref object receives the mounted instance of the component as its current (not discussed in this document). +- **You may not use the ref attribute on component functions** because they don’t have instances (we don't expose JS classes in ReScript). + +Here are some examples: + +### Adding a Ref to a DOM Element + +This code uses a `React.ref` to store a reference to an `input` DOM node to put focus on a text field when a button was clicked: + + + +```res +// CustomTextInput.res + +@bs.send external focus: Dom.element => unit = "focus" + +@react.component +let make = () => { + let textInput = React.useRef(Js.Nullable.null) + + let focusInput = () => + switch textInput.current->Js.Nullable.toOption { + | Some(dom) => dom->focus + | None => () + } + + let onClick = _ => focusInput() + +
+ + +
+} +``` + +```js +function CustomTextInput(Props) { + var textInput = React.useRef(null); + var onClick = function (param) { + var dom = textInput.current; + if (!(dom == null)) { + dom.focus(); + return ; + } + + }; + return React.createElement("div", undefined, React.createElement("input", { + ref: textInput, + type: "text" + }), React.createElement("input", { + type: "button", + value: "Focus the text input", + onClick: onClick + })); +} +``` + +
+ +A few things happened here, so let's break them down: + +- We initialize our `textInput` ref as a `Js.Nullable.null` +- We register our `textInput` ref in our `` element with `ReactDOM.Ref.domRef(textInput)` +- In our `focusInput` function, we need to first verify that our DOM element is set, and then use the `focus` binding to set the focus + +React will assign the `current` field with the DOM element when the component mounts, and assign it back to null when it unmounts. + + +### Refs and Component Functions + +In React, you **can't** pass a `ref` attribute to a component function: + + + + +```res +module MyComp = { + @react.component + let make = (~ref) => +} + +@react.component +let make = () => { + let textInput = React.useRef(Js.Nullable.null) + + // This will **not** work + +} +``` + +```js +// Compiler Error: +// Ref cannot be passed as a normal prop. Please use `forwardRef` +// API instead +``` + + + +The snippet above will not compile and output an error that looks like this: `"Ref cannot be passed as a normal prop. Please use forwardRef API instead."`. + +As the error message implies, If you want to allow people to take a ref to your component function, you can use [ref forwarding](./forwarding-refs) (possibly in conjunction with useImperativeHandle) instead. + + +## Exposing DOM Refs to Parent Components + +In rare cases, you might want to have access to a child’s DOM node from a parent component. This is generally not recommended because it breaks component encapsulation, but it can occasionally be useful for triggering focus or measuring the size or position of a child DOM node. + +we recommend to use [ref forwarding](./forwarding-refs) for these cases. **Ref forwarding lets components opt into exposing any child component’s ref as their own**. You can find a detailed example of how to expose a child’s DOM node to a parent component in the ref forwarding documentation. + +## Callback Refs + +React also supports another way to set refs called “callback refs” (`React.Ref.callbackDomRef`), which gives more fine-grain control over when refs are set and unset. + +Instead of passing a ref value created by `React.useRef()`, you can pass in a callback function. The function receives the target `Dom.element` as its argument, which can be stored and accessed elsewhere. + +**Note:** Usually we'd use `React.Ref.domRef()` to pass a ref value, but for callback refs, we use `React.Ref.callbackDomRef()` instead. + +The example below implements a common pattern: using the ref callback to store a reference to a DOM node in an instance property. + + + +```res +// CustomTextInput.re + +@bs.send external focus: Dom.element => unit = "focus" + +@react.component +let make = () => { + let textInput = React.useRef(Js.Nullable.null) + let setTextInputRef = element => { + textInput.current = element; + } + + let focusTextInput = _ => { + textInput.current + ->Js.Nullable.toOption + ->Belt.Option.forEach(input => input->focus) + } + +
+ + +
+} +``` + +```js +function CustomTextInput(Props) { + var textInput = React.useRef(null); + var setTextInputRef = function (element) { + textInput.current = element; + + }; + var focusTextInput = function (param) { + return Belt_Option.forEach(Caml_option.nullable_to_opt(textInput.current), (function (input) { + input.focus(); + + })); + }; + return React.createElement("div", undefined, React.createElement("input", { + ref: setTextInputRef, + type: "text" + }), React.createElement("input", { + type: "button", + value: "Focus the text input", + onClick: focusTextInput + })); +} +``` + +
+ +React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts. + +You can pass callback refs between components like you can with object refs that were created with `React.useRef()`. + + + +```res +// Parent.res + +@bs.send external focus: Dom.element => unit = "focus" + +module CustomTextInput = { + @react.component + let make = (~setInputRef) => { +
+ +
+ } +} + +@react.component +let make = () => { + let textInput = React.useRef(Js.Nullable.null) + let setInputRef = element => { textInput.current = element} + + +} +``` + +```js +function CustomTextInput(Props) { + var setInputRef = Props.setInputRef; + return React.createElement("div", undefined, React.createElement("input", { + ref: setInputRef, + type: "text" + })); +} + +var CustomTextInput = { + make: CustomTextInput +}; + +function Parent(Props) { + var textInput = React.useRef(null); + var setInputRef = function (element) { + textInput.current = element; + + }; + return React.createElement(CustomTextInput, { + setInputRef: setInputRef + }); +} +``` + +
+ + +In the example above, `Parent` passes its ref callback as an `setInputRef` prop to the `CustomTextInput`, and the `CustomTextInput` passes the same function as a special ref attribute to the ``. As a result, the `textInput` ref in Parent will be set to the DOM node corresponding to the `` element in the `CustomTextInput`. + diff --git a/scripts/extract-tocs.js b/scripts/extract-tocs.js index 44a37330b..cfdfe388d 100644 --- a/scripts/extract-tocs.js +++ b/scripts/extract-tocs.js @@ -154,10 +154,12 @@ const createReactToc = () => { "rendering-elements", "components-and-props", "arrays-and-keys", + "refs-and-the-dom", "hooks-overview", "hooks-state", "hooks-effect", "hooks-context", + "hooks-ref", ]; const files = glob.sync(`${MD_DIR}/*.md?(x)`); From 422af5fd3d1ac837a6ced673eeca44dbbad39cff Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Tue, 29 Sep 2020 12:16:50 +0200 Subject: [PATCH 13/29] Add useReducer docs --- pages/docs/react/latest/hooks-reducer.mdx | 357 ++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 pages/docs/react/latest/hooks-reducer.mdx diff --git a/pages/docs/react/latest/hooks-reducer.mdx b/pages/docs/react/latest/hooks-reducer.mdx new file mode 100644 index 000000000..c777e1b14 --- /dev/null +++ b/pages/docs/react/latest/hooks-reducer.mdx @@ -0,0 +1,357 @@ +--- +title: useReducer Hook +description: "Details about the useReducer React hook in ReScript" +canonical: "/docs/react/latest/hooks-reducer" +category: "Hooks & State Management" +--- + +# useReducer + + + +`React.useReducer` helps you express your state in an action / reducer pattern. + + + +## Usage + + + +```res +let (state, dispatch) = React.useReducer(reducer, initialState) +``` + +```js +var match = React.useReducer(reducer, initialState); +``` + + + +An alternative to [useState](./hooks-state). Accepts a reducer of type `(state, action) => newState`, and returns the current `state` paired with a `dispatch` function `(action) => unit`. + +`React.useReducer` is usually preferable to `useState` when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. `useReducer` also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks. + +**Note:** You will notice that the action / reducer pattern works especially well in ReScript due to its [immutable records](/docs/manual/latest/record), [variants](/docs/manual/latest/variant) and [pattern matching](/docs/manual/pattern-matching-destructuring) features for easy expression of your action and state transitions. + +## Examples + +### Counter Example with `React.useReducer` + + + +```res +// Counter.res + +type action = Inc | Dec +type state = {count: int} + +let reducer = (state, action) => { + switch action { + | Inc => {count: state.count + 1} + | Dec => {count: state.count - 1} + } +} + +@react.component +let make = () => { + let (state, dispatch) = React.useReducer(reducer, {count: 0}) + + <> + {React.string("Count:" ++ Belt.Int.toString(state.count))} + + + +} +``` + +```js +function reducer(state, action) { + if (action) { + return { + count: state.count - 1 | 0 + }; + } else { + return { + count: state.count + 1 | 0 + }; + } +} + +function Counter(Props) { + var match = React.useReducer(reducer, { + count: 0 + }); + var dispatch = match[1]; + return React.createElement(React.Fragment, undefined, "Count:" + String(match[0].count), React.createElement("button", { + onClick: (function (param) { + return Curry._1(dispatch, /* Dec */1); + }) + }, "-"), React.createElement("button", { + onClick: (function (param) { + return Curry._1(dispatch, /* Inc */0); + }) + }, "+")); +} +``` + + + +> React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. + +### Basic Todo List App with More Complex Actions + +You can leverage the full power of variants to express actions with data payloads to parametrize your state transitions: + + + +```res +// TodoApp.res + +type todo = { + id: int, + content: string, + completed: bool, +} + +type action = + | AddTodo(string) + | RemoveTodo(int) + | ToggleTodo(int) + +type state = { + todos: array, + nextId: int, +} + +let reducer = (state, action) => + switch action { + | AddTodo(content) => + let todos = Js.Array2.concat( + state.todos, + [{id: state.nextId, content: content, completed: false}], + ) + {todos: todos, nextId: state.nextId + 1} + | RemoveTodo(id) => + let todos = Js.Array2.filter(state.todos, todo => todo.id !== id) + {...state, todos: todos} + | ToggleTodo(id) => + let todos = Belt.Array.map(state.todos, todo => + if todo.id === id { + { + ...todo, + completed: !todo.completed, + } + } else { + todo + } + ) + {...state, todos: todos} + } + +let initialTodos = [{id: 1, content: "Try ReScript & React", completed: false}] + +@react.component +let make = () => { + let (state, dispatch) = React.useReducer( + reducer, + {todos: initialTodos, nextId: 2}, + ) + + let todos = Belt.Array.map(state.todos, todo => +
  • + {React.string(todo.content)} + + dispatch(ToggleTodo(todo.id))} + /> +
  • + ) + + <>

    {React.string("Todo List:")}

      {React.array(todos)}
    +} +``` + +```js +function reducer(state, action) { + switch (action.TAG | 0) { + case /* AddTodo */0 : + var todos = state.todos.concat([{ + id: state.nextId, + content: action._0, + completed: false + }]); + return { + todos: todos, + nextId: state.nextId + 1 | 0 + }; + case /* RemoveTodo */1 : + var id = action._0; + var todos$1 = state.todos.filter(function (todo) { + return todo.id !== id; + }); + return { + todos: todos$1, + nextId: state.nextId + }; + case /* ToggleTodo */2 : + var id$1 = action._0; + var todos$2 = Belt_Array.map(state.todos, (function (todo) { + if (todo.id === id$1) { + return { + id: todo.id, + content: todo.content, + completed: !todo.completed + }; + } else { + return todo; + } + })); + return { + todos: todos$2, + nextId: state.nextId + }; + + } +} + +var initialTodos = [{ + id: 1, + content: "Try ReScript & React", + completed: false + }]; + +function TodoApp(Props) { + var match = React.useReducer(reducer, { + todos: initialTodos, + nextId: 2 + }); + var dispatch = match[1]; + var todos = Belt_Array.map(match[0].todos, (function (todo) { + return React.createElement("li", undefined, todo.content, React.createElement("button", { + onClick: (function (param) { + return Curry._1(dispatch, { + TAG: /* RemoveTodo */1, + _0: todo.id + }); + }) + }, "Remove"), React.createElement("input", { + checked: todo.completed, + type: "checkbox", + onChange: (function (param) { + return Curry._1(dispatch, { + TAG: /* ToggleTodo */2, + _0: todo.id + }); + }) + })); + })); + return React.createElement(React.Fragment, undefined, React.createElement("h1", undefined, "Todo List:"), React.createElement("ul", undefined, todos)); +} +``` + +
    + + +## Lazy Initialization + + + + + +```res +let (state, dispatch) = + React.useReducerWithMapState(reducer, initialState, initial) +``` + +```js +var match = React.useReducer(reducer, initialState, init); +``` + + + +You can also create the `initialState` lazily. To do this, you can use `React.useReducerWithMapState` and pass an `init` function as the third argument. The initial state will be set to `init(initialState)`. + +It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action: + + + +```res +// Counter.res + +type action = Inc | Dec | Reset(int) +type state = {count: int} + +let init = initialCount => { + {count: initialCount} +} + +let reducer = (state, action) => { + switch action { + | Inc => {count: state.count + 1} + | Dec => {count: state.count - 1} + | Reset(count) => init(count) + } +} + +@react.component +let make = (~initialCount: int) => { + let (state, dispatch) = React.useReducerWithMapState( + reducer, + initialCount, + init, + ) + + <> + {React.string("Count:" ++ Belt.Int.toString(state.count))} + + + +} +``` + +```js +function init(initialCount) { + return { + count: initialCount + }; +} + +function reducer(state, action) { + if (typeof action === "number") { + if (action !== 0) { + return { + count: state.count - 1 | 0 + }; + } else { + return { + count: state.count + 1 | 0 + }; + } + } else { + return { + count: action._0 + }; + } +} + +function Counter(Props) { + var initialCount = Props.initialCount; + var match = React.useReducer(reducer, initialCount, init); + var dispatch = match[1]; + return React.createElement(React.Fragment, undefined, "Count:" + String(match[0].count), React.createElement("button", { + onClick: (function (param) { + return Curry._1(dispatch, /* Dec */1); + }) + }, "-"), React.createElement("button", { + onClick: (function (param) { + return Curry._1(dispatch, /* Inc */0); + }) + }, "+")); +} +``` + + From 9a2af56f7388eb3bc2aedd6adabea94f754b33ec Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Wed, 30 Sep 2020 16:11:11 +0200 Subject: [PATCH 14/29] Improve hljs line highlighting --- common/HighlightJs.re | 7 +++++-- styles/_hljs.css | 4 ---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/common/HighlightJs.re b/common/HighlightJs.re index d2bb313fe..a5716cd8e 100644 --- a/common/HighlightJs.re +++ b/common/HighlightJs.re @@ -25,9 +25,12 @@ let renderHLJS = Js.String2.split(highlighted, "\n") ->Belt.Array.mapWithIndex((i, line) => if (Js.Array2.find(highlightedLines, lnum => lnum === i + 1) !== None) { - "" ++ line ++ ""; + let content = line === "" ? " " : line; + "" ++ content ++ ""; } else { - line; + "" + ++ line + ++ ""; } ) ->Js.Array2.joinWith("\n"); diff --git a/styles/_hljs.css b/styles/_hljs.css index b39841336..988b72aee 100644 --- a/styles/_hljs.css +++ b/styles/_hljs.css @@ -114,10 +114,6 @@ Docco style used in http://jashkenas.github.com/docco/ converted by Simon Madine /*@apply text-code-1;*/ } -.hljs-line-highlight { - @apply w-full inline-block bg-berry-15; -} - /* DARK MODE COLORS */ .hljs.dark .hljs-comment { From 320c43df1fff9dcea9155cff161d5a4def0f123c Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Wed, 30 Sep 2020 16:11:47 +0200 Subject: [PATCH 15/29] Remove cruft from rendering-elements --- pages/docs/react/latest/rendering-elements.mdx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pages/docs/react/latest/rendering-elements.mdx b/pages/docs/react/latest/rendering-elements.mdx index 98e0d08d4..e3aab2133 100644 --- a/pages/docs/react/latest/rendering-elements.mdx +++ b/pages/docs/react/latest/rendering-elements.mdx @@ -123,11 +123,6 @@ React.createElement("div", undefined, element); -### Creating Variadic Elements - - - - ## Cloning Elements **Note:** This is an escape hatch feature and will only be useful for interoping with existing JS code / libraries. From d20ba1968a9671d4d069a4f5b340dc80c6ccd9c3 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Wed, 30 Sep 2020 16:12:00 +0200 Subject: [PATCH 16/29] Add custom hooks docs --- pages/docs/react/latest/hooks-custom.mdx | 538 +++++++++++++++++++++++ 1 file changed, 538 insertions(+) create mode 100644 pages/docs/react/latest/hooks-custom.mdx diff --git a/pages/docs/react/latest/hooks-custom.mdx b/pages/docs/react/latest/hooks-custom.mdx new file mode 100644 index 000000000..e04fb3852 --- /dev/null +++ b/pages/docs/react/latest/hooks-custom.mdx @@ -0,0 +1,538 @@ +--- +title: Build A Custom Hook +description: "How to build your own hooks in ReScript & React" +canonical: "/docs/react/latest/hooks-custom" +category: "Hooks & State Management" +--- + +# Build A Custom Hook + + + +React comes with a few fundamental hooks out-of-the-box, such as `React.useState` or `React.useEffect`. Here you will learn how to build your own, higher-level hooks for your React use-cases. + + + +## Why Custom Hooks? + +Custom hooks let you extract existing component logic into reusable, separate functions. + +Let's go back to a previous example from our [React.useEffect section](./hooks-effect) where we built a component for a chat application that displays a message, indicating whether a friend is online or offline: + + + +```res {16-31} +// FriendStatus.res + +module ChatAPI = { + // Imaginary globally available ChatAPI for demo purposes + type status = { isOnline: bool }; + @bs.val external subscribeToFriendStatus: (string, status => unit) => unit = "subscribeToFriendStatus"; + @bs.val external unsubscribeFromFriendStatus: (string, status => unit) => unit = "unsubscribeFromFriendStatus"; +} + +type state = Offline | Loading | Online; + +@react.component +let make = (~friendId: string) => { + let (state, setState) = React.useState(_ => Offline) + + React.useEffect(() => { + let handleStatusChange = (status) => { + setState(_ => { + status.ChatAPI.isOnline ? Online : Offline + }) + } + + ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange); + setState(_ => Loading); + + let cleanup = () => { + ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange) + } + + Some(cleanup) + }) + + let text = switch(state) { + | Offline => friendId ++ " is offline" + | Online => friendId ++ " is online" + | Loading => "loading..." + } + +
    + {React.string(text)} +
    +} + +``` + +```js +function FriendStatus(Props) { + var friendId = Props.friendId; + var match = React.useState(function () { + return /* Offline */0; + }); + var setState = match[1]; + React.useEffect(function () { + var handleStatusChange = function (status) { + return Curry._1(setState, (function (param) { + if (status.isOnline) { + return /* Online */2; + } else { + return /* Offline */0; + } + })); + }; + subscribeToFriendStatus(friendId, handleStatusChange); + Curry._1(setState, (function (param) { + return /* Loading */1; + })); + return (function (param) { + unsubscribeFromFriendStatus(friendId, handleStatusChange); + + }); + }); + var text; + switch (match[0]) { + case /* Offline */0 : + text = friendId + " is offline"; + break; + case /* Loading */1 : + text = "loading..."; + break; + case /* Online */2 : + text = friendId + " is online"; + break; + + } + return React.createElement("div", undefined, text); +} +``` + +
    + +Now let’s say that our chat application also has a contact list, and we want to render names of online users with a green color. We could copy and paste similar logic above into our `FriendListItem` component but it wouldn’t be ideal: + + + +```res {15-30} +// FriendListItem.res +type state = Offline | Loading | Online; + +// module ChatAPI = {...} + +type friend = { + id: string, + name: string +}; + +@react.component +let make = (~friend: friend) => { + let (state, setState) = React.useState(_ => Offline) + + React.useEffect(() => { + let handleStatusChange = (status) => { + setState(_ => { + status.ChatAPI.isOnline ? Online : Offline + }) + } + + ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange); + setState(_ => Loading); + + let cleanup = () => { + ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange) + } + + Some(cleanup) + }) + + let color = switch(state) { + | Offline => "red" + | Online => "green" + | Loading => "grey" + } + +
  • + {React.string(friend.name)} +
  • +} +``` + +```js +function FriendListItem(Props) { + var friend = Props.friend; + var match = React.useState(function () { + return /* Offline */0; + }); + var setState = match[1]; + React.useEffect(function () { + var handleStatusChange = function (status) { + return Curry._1(setState, (function (param) { + if (status.isOnline) { + return /* Online */2; + } else { + return /* Offline */0; + } + })); + }; + subscribeToFriendStatus(friend.id, handleStatusChange); + Curry._1(setState, (function (param) { + return /* Loading */1; + })); + return (function (param) { + unsubscribeFromFriendStatus(friend.id, handleStatusChange); + + }); + }); + var color; + switch (match[0]) { + case /* Offline */0 : + color = "red"; + break; + case /* Loading */1 : + color = "grey"; + break; + case /* Online */2 : + color = "green"; + break; + + } + return React.createElement("li", { + style: { + color: color + } + }, friend.name); +} +``` + +
    + +Instead, we’d like to share this logic between `FriendStatus` and `FriendListItem`. + +Traditionally in React, we’ve had two popular ways to share stateful logic between components: render props and higher-order components. We will now look at how Hooks solve many of the same problems without forcing you to add more components to the tree. + +## Extracting a Custom Hook + +Usually when we want to share logic between two functions, we extract it to a third function. Both components and Hooks are functions, so this works for them too! + +**A custom Hook is a function whose name starts with ”use” and that may call other Hooks.** For example, `useFriendStatus` below is our first custom Hook (we create a new file `FriendStatusHook.res` to encapsulate the `state` type as well): + + + + + +```res +// FriendStatusHook.res + +// module ChatAPI {...} + +type state = Offline | Loading | Online + +let useFriendStatus = (friendId: string): state => { + let (state, setState) = React.useState(_ => Offline) + + React.useEffect(() => { + let handleStatusChange = status => { + setState(_ => { + status.ChatAPI.isOnline ? Online : Offline + }) + } + + ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange) + setState(_ => Loading) + + let cleanup = () => { + ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange) + } + + Some(cleanup) + }) + + state +} +``` + +```js +function useFriendStatus(friendId) { + var match = React.useState(function () { + return /* Offline */0; + }); + var setState = match[1]; + React.useEffect(function () { + var handleStatusChange = function (status) { + return Curry._1(setState, (function (param) { + if (status.isOnline) { + return /* Online */2; + } else { + return /* Offline */0; + } + })); + }; + subscribeToFriendStatus(friendId, handleStatusChange); + Curry._1(setState, (function (param) { + return /* Loading */1; + })); + return (function (param) { + unsubscribeFromFriendStatus(friendId, handleStatusChange); + + }); + }); + return match[0]; +} +``` + + + +There’s nothing new inside of it — the logic is copied from the components above. Just like in a component, make sure to only call other Hooks unconditionally at the top level of your custom Hook. + +Unlike a React component, a custom Hook doesn’t need to have a specific signature. We can decide what it takes as arguments, and what, if anything, it should return. In other words, it’s just like a normal function. Its name should always start with use so that you can tell at a glance that the rules of Hooks apply to it. + +The purpose of our `useFriendStatus` Hook is to subscribe us to a friend’s status. This is why it takes `friendId` as an argument, and returns the online state like `Online`, `Offline` or `Loading`: + +```res +let useFriendStatus = (friendId: string): status { + let (state, setState) = React.useState(_ => Offline); + + // ... + + state +} +``` + +Now let’s use our custom Hook. + + +## Using a Custom Hook + +In the beginning, our stated goal was to remove the duplicated logic from the `FriendStatus` and `FriendListItem` components. Both of them want to know the online state of a friend. + + +Now that we’ve extracted this logic to a useFriendStatus hook, we can just use it: + + + + +```res {6} +// FriendStatus.res +type friend = { id: string }; + +@react.component +let make = (~friend: friend) => { + let onlineState = FriendStatusHook.useFriendStatus(friend.id); + + let status = switch(onlineState) { + | FriendStatusHook.Online => "Online" + | Loading => "Loading" + | Offline => "Offline" + } + + React.string(status); +} +``` +```js +function FriendStatus(Props) { + var friend = Props.friend; + var onlineState = useFriendStatus(friend.id); + var color; + switch (onlineState) { + case /* Offline */0 : + color = "red"; + break; + case /* Loading */1 : + color = "grey"; + break; + case /* Online */2 : + color = "green"; + break; + + } + return React.createElement("li", { + style: { + color: color + } + }, friend.name); +} +``` + + + + + +```res {4} +// FriendListItem.res +@react.component +let make = (~friend: friend) => { + let onlineState = FriendStatusHook.useFriendStatus(friend.id); + + let color = switch(onlineState) { + | Offline => "red" + | Online => "green" + | Loading => "grey" + } + +
  • + {React.string(friend.name)} +
  • +} +``` + +```js +function FriendListItem(Props) { + var friend = Props.friend; + var onlineState = useFriendStatus(friend.id); + var color; + switch (onlineState) { + case /* Offline */0 : + color = "red"; + break; + case /* Loading */1 : + color = "grey"; + break; + case /* Online */2 : + color = "green"; + break; + + } + return React.createElement("li", { + style: { + color: color + } + }, friend.name); +} +``` + +
    + + +**Is this code equivalent to the original examples?** Yes, it works in exactly the same way. If you look closely, you’ll notice we didn’t make any changes to the behavior. All we did was to extract some common code between two functions into a separate function. Custom Hooks are a convention that naturally follows from the design of Hooks, rather than a React feature. + +**Do I have to name my custom Hooks starting with “use”?** Please do. This convention is very important. Without it, we wouldn’t be able to automatically check for violations of rules of Hooks because we couldn’t tell if a certain function contains calls to Hooks inside of it. + +**Do two components using the same Hook share state?** No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated. + +**How does a custom Hook get isolated state?** Each call to a Hook gets isolated state. Because we call useFriendStatus directly, from React’s point of view our component just calls useState and useEffect. And as we learned earlier, we can call useState and useEffect many times in one component, and they will be completely independent. + + +### Tip: Pass Information Between Hooks + +Since Hooks are functions, we can pass information between them. + +To illustrate this, we’ll use another component from our hypothetical chat example. This is a chat message recipient picker that displays whether the currently selected friend is online: + + + +```res {11,12,14-18,22} +type friend = {id: string, name: string} + +let friendList = [ + {id: "1", name: "Phoebe"}, + {id: "2", name: "Rachel"}, + {id: "3", name: "Ross"}, +] + +@react.component +let make = () => { + let (recipientId, setRecipientId) = React.useState(_ => "1") + let recipientOnlineState = FriendStatusHook.useFriendStatus(recipientId) + + let color = switch recipientOnlineState { + | FriendStatusHook.Offline => Circle.Red + | Online => Green + | Loading => Grey + } + + let onChange = evt => { + let value = ReactEvent.Form.target(evt)["value"] + setRecipientId(value) + } + + let friends = Belt.Array.map(friendList, friend => { + + }) + + <> + + + +} +``` +```js +var friendList = [ + { + id: "1", + name: "Phoebe" + }, + { + id: "2", + name: "Rachel" + }, + { + id: "3", + name: "Ross" + } +]; + +function Playground(Props) { + var match = React.useState(function () { + return "1"; + }); + var setRecipientId = match[1]; + var recipientId = match[0]; + var recipientOnlineState = useFriendStatus(recipientId); + var color; + switch (recipientOnlineState) { + case /* Offline */0 : + color = /* Red */0; + break; + case /* Loading */1 : + color = /* Grey */2; + break; + case /* Online */2 : + color = /* Green */1; + break; + + } + var onChange = function (evt) { + return Curry._1(setRecipientId, evt.target.value); + }; + var friends = Belt_Array.map(friendList, (function (friend) { + return React.createElement("option", { + key: friend.id, + value: friend.id + }, friend.name); + })); + return React.createElement(React.Fragment, undefined, React.createElement(Playground$Circle, { + color: color + }), React.createElement("select", { + value: recipientId, + onChange: onChange + }, friends)); +} +``` + + + +We keep the currently chosen friend ID in the `recipientId` state variable, and update it if the user chooses a different friend in the ``) aren't syntactically valid; `type` is a reserved keyword in ReScript. Use `` instead. + +For `aria-*` use camelCasing, e.g., `ariaLabel`. For DOM components, we'll translate it to `aria-label` under the hood. + +For `data-*` this is a bit trickier; words with `-` in them aren't valid in ReScript. When you do want to write them, e.g., `
    `, check out the [React.cloneElement](./rendering-elements#cloning-elements) or [React.createDOMElementVariadic](./rendering-elements#creating-dom-elements) section. + + ## Children Props In React `props.children` is a special attribute to represent the nested elements within a parent element: From ab39c5b730b67feeddad471675fd2e581f30da9f Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sun, 1 Nov 2020 12:50:22 +0100 Subject: [PATCH 20/29] Refine beyond-jsx, components-and-props, rendering-elements --- pages/docs/react/latest/beyond-jsx.mdx | 4 +- .../react/latest/components-and-props.mdx | 4 +- pages/docs/react/latest/elements-and-jsx.mdx | 225 ++++++++++++++++++ .../docs/react/latest/rendering-elements.mdx | 217 ++--------------- scripts/extract-tocs.js | 1 + 5 files changed, 247 insertions(+), 204 deletions(-) create mode 100644 pages/docs/react/latest/elements-and-jsx.mdx diff --git a/pages/docs/react/latest/beyond-jsx.mdx b/pages/docs/react/latest/beyond-jsx.mdx index eb7b22263..841f730e0 100644 --- a/pages/docs/react/latest/beyond-jsx.mdx +++ b/pages/docs/react/latest/beyond-jsx.mdx @@ -2,7 +2,7 @@ title: Beyond JSX description: "Details on how to use ReScript and React without JSX" canonical: "/docs/react/latest/beyond-jsx" -category: "Main Concepts" +category: "Guides" --- # Beyond JSX @@ -13,7 +13,7 @@ JSX is a syntax sugar that allows us to use React components in an HTML like man -**Note:** This section requires knowledge about the apis for [creating elements](./rendering-elements#creating-elements-from-component-functions), such as `React.createElement` or `ReactDOMRe.createDOMElementVariadic`. +**Note:** This section requires knowledge about the low level apis for [creating elements](./elements-and-jsx#creating-elements-from-component-functions), such as `React.createElement` or `ReactDOMRe.createDOMElementVariadic`. ## Component Types diff --git a/pages/docs/react/latest/components-and-props.mdx b/pages/docs/react/latest/components-and-props.mdx index c5916d84f..e9d974904 100644 --- a/pages/docs/react/latest/components-and-props.mdx +++ b/pages/docs/react/latest/components-and-props.mdx @@ -19,7 +19,7 @@ A React component is a function describing a UI element that receives a `props` The nice thing about this concept is that you can solely focus on the input and output. The component function receives some data and returns some opaque `React.element` that is managed by the React framework to render your UI. -> If you want to know more about the low level details on how a component interface is implemented, refer to the advanced topic [React JSX Transformation](./react-jsx-transformation) +> If you want to know more about the low level details on how a component interface is implemented, refer to the [Beyond JSX](./beyond-jsx) page. ## Component Example @@ -56,7 +56,7 @@ You can also see in the the JS output that the function we created was directly ## Defining Props -In ReactJS, props are usually described as a single `props` object. In ReScript, we use [labeled arguments](docs/manual/latest/function#labeled-arguments) to define our props parameters instead. Here's an example: +In ReactJS, props are usually described as a single `props` object. In ReScript, we use [labeled arguments](/docs/manual/latest/function#labeled-arguments) to define our props parameters instead. Here's an example: diff --git a/pages/docs/react/latest/elements-and-jsx.mdx b/pages/docs/react/latest/elements-and-jsx.mdx new file mode 100644 index 000000000..0271a8e06 --- /dev/null +++ b/pages/docs/react/latest/elements-and-jsx.mdx @@ -0,0 +1,225 @@ +--- +title: Elements & JSX +description: "Basic concepts for React elements and how to use them in JSX" +canonical: "/docs/react/latest/elements-and-jsx" +category: "Main Concepts" +--- + +# Elements & JSX + + + +Elements are the smallest building blocks of React apps. This page will explain how to handle `React.element`s in your React app with our dedicated JSX syntax. + + + +> **Note:** This page assumes your `bsconfig.json` to be set to `"reason": { "react-jsx": 3 }`, otherwise your JSX will not be transformed to its React specific form. + +## Element Basics + +Let's start out by creating our first React element. + +```res +let element =

    {React.string("Hello World")}

    +``` + +The binding `element` and the expression `{React.string("Hello World")}` are both of type `React.element`, the fundamental type for representing React elements within a React application. An element describes what you see on the screen whenever you render your application to the DOM. + +Let's say you want to create a function that handles another React element, such as `children`, you can annotate it as `React.element`: + +```res +let wrapChildren = (children: React.element) => { +
    +

    {React.string("Overview")}

    + children +
    +} + +wrapChildren(
    React.string("Let's use React with ReScript")
    ) +``` + +Understanding the definition of a `React.element` is essential since it is heavily used within the React APIs, such as `ReactDOM.render(element, ...)`, etc. Be aware that JSX doesn't do any automatic `string` to `React.element` conversion for you (ReScript forces explicit type conversion). For example `
    Hello World
    ` will not type-check (which is actually a good thing because it's also a huge source for subtle bugs!), you need to convert your `"Hello World"` with the `React.string` function first. + +Fortunately our React bindings bring all necessary functionality to represent all relevant data types as `React.element`s. + +## Using Elements within JSX + +You can compose elements into more complex structures by using JSX: + +```res +let greeting = React.string("Hello ") +let name = React.string("Stranger"); + + +// element is also of type React.element +let element =
    greeting name
    +``` + +JSX is the main way to express your React application as a tree of elements. + +Sometimes, when doing a lot of interop with existing ReactJS codebases, you'll find yourself in a situation where you can't use JSX syntax due to syntactic restrictions. Check out the [Escape Hatches](#escape-hatches) chapter later on for workarounds. + +## Creating Elements + +### Creating Elements from `string`, `int`, `float`, `array` + +Apart from using JSX to create our React elements or React components, the `React` module offers various functions to create elements from primitive data types: + +```res +React.string("Hello") // new element representing "Hello" + +React.int(1) // new element representing "1" + +React.float(1.0) // new element representing "1.0" +``` + +It also offers `React.array` to represent multiple elements as one single element (useful for rendering a list of data, or passing children): + +```res +let element = React.array([ + React.string("element 1"), + React.string("element 2"), + React.string("element 3") +]) +``` + +**Note:** We don't offer a `React.list` function because a `list` value would impose runtime overhead. ReasonReact cares about clean, idiomatic JS output. If you want to transform a `list` of elements to a single React element, combine the output of `Belt.List.toArray` with `React.array` instead. + +### Creating Null Elements + +ReScript doesn't allow `element || null` constraints due to it's strongly typed nature. Whenever you are expressing conditionals where a value might, or might not be rendered, you will need the `React.null` constant to represent *Nothingness*: + + + + +```res +let name = Some("Andrea") + +let element = switch name { + | Some(name) =>
    {React.string("Hello " ++ name)}
    + | None => React.null +} + +
    element
    +``` +```js +var name = "Andrea"; + +var element = name !== undefined ? React.createElement("div", undefined, "Hello " + name) : null; + +React.createElement("div", undefined, element); +``` + +
    + +## Escape Hatches + +**Note:** This chapter features low level APIs that are used by JSX itself, and should only be used whenever you hit certain JSX syntax limitations. More infos on the JSX internals can be found in our [Beyond JSX](./beyond-jsx) section. + +### Creating Elements from Component Functions + +**Note:** Details on components and props will be described in the [next chapter](./components-and-props). + +Sometimes it's necessary to pass around component functions to have more control over `React.element` creation. Use the `React.createElement` function to instantiate your elements: + +```res +type props = {"name": string}; + +let render = (myComp: props => React.element) => { +
    + {React.createElement(myComp, {"name": "Franz"})} +
    +} +``` + +This feature is often used when interacting with existing JS / ReactJS code. In pure ReScript React applications, you would rather pass a function that does the rendering for you (also called a "render prop"): + +```res +let render = (renderMyComp: (~name: string) => React.element) => { +
    + {renderMyComp("Franz")} +
    +} +``` + +#### Pass Variadic Children + +There is also a `React.createElementVariadic` function, which takes an array of children as a third parameter: + + + +```res +type props = {"title": string, "children": React.element}; + +let render = (article: props => React.element) => { + let children = [React.string("Introduction"), React.string("Body")]; + + let props = {"title": "Article #1", "children": React.null}; + + {React.createElementVariadic(article, props, children)} +} +``` +```js +function render(article) { + var children = ["Introduction"]; + var props = { + title: "Article #1", + children: null + }; + return Caml_splice_call.spliceApply(React.createElement, [ + article, + props, + children + ]); +} +``` + + + +**Note:** Here we are passing a prop `"children": React.null` to satisfy the type checker. React will ignore the children prop in favor of the children array. + +This function is mostly used by our JSX transformations, so usually you want to use `React.createElement` and pass a children prop instead. + +### Creating DOM Elements + + +To create DOM elements (`
    `, ``, etc.), use `ReactDOMRe.createDOMElementVariadic`: + +```res +ReactDOMRe.createDOMElementVariadic("div", ~props=ReactDOM.domProps(~className="card", ()), []); +``` + +The function above requires the `ReactDOM.domProps` constructor function, so ReScript can make sure that we are only passing valid dom props. You can find an exhaustive list of all available props in the [ReactDOM](https://github.com/reasonml/reason-react/blob/master/src/ReactDOM.re#L61) module. + + +### Cloning Elements + +**Note:** This is an escape hatch feature and will only be useful for interoping with existing JS code / libraries. + +Sometimes it's required to clone an existing element to set, overwrite or add prop values to a new instance, or if you want to set invalid prop names such as `data-name`. You can use `React.cloneElement` for that: + + + +```res +let original =
    + +// Will return a new React.element with className set to "world" +React.cloneElement(original, {"className": "world", "data-name": "some name"}); +``` +```js +var original = React.createElement("div", { + className: "hello" + }); + +React.cloneElement(original, { + className: "world", + "data-name": "some name" + }); +``` + + + +The feature mentioned above could also replicate `props spreading`, a practise commonly used in ReactJS codebases, but we strongly discourage the usage due to its unsafe nature and its incorrectness (e.g. adding undefined extra props to a component doesn't make sense, and causes hard to find bugs). + +In ReScript, we rather pass down required props explicitly to leaf components or use a renderProp instead. We introduced [JSX punning](/docs/manual/latest/jsx#punning) syntax to make the process of passing down props more convenient. + diff --git a/pages/docs/react/latest/rendering-elements.mdx b/pages/docs/react/latest/rendering-elements.mdx index 938c8f2dd..fd5f3497a 100644 --- a/pages/docs/react/latest/rendering-elements.mdx +++ b/pages/docs/react/latest/rendering-elements.mdx @@ -1,6 +1,6 @@ --- title: Rendering Elements -description: "Basic concepts for React elements and how to render them" +description: "How to render React elements to the DOM" canonical: "/docs/react/latest/rendering-elements" category: "Main Concepts" --- @@ -9,220 +9,35 @@ category: "Main Concepts" -Elements are the smallest building blocks of React apps. This page will explain how to handle `React.element`s in your React app and describes all important APIs to create, manipulate and render elements to the screen. +In our previous section [React Elements & JSX](./elements-and-jsx) we learned how to create and handle React elements. In this section we will learn how to put our elements into action by rendering them into the DOM. -> **Note:** This page assumes your `bsconfig.json` to be set to `"reason": { "react-jsx": 3 }`, otherwise your JSX will not be transformed to its React specific form. - -## Element Basics - -Let's start out by creating our first React element. +As we mentioned before, a `React.element` describes what you see on the screen: ```res let element =

    {React.string("Hello World")}

    ``` -The binding `element` and the expression `{React.string("Hello World")}` are both of type `React.element`, the fundamental type for representing React elements within your React application. - -Let's say you want to create a function that handles another React element, such as `children`, you can annotate it as `React.element`: - -```res -let wrapChildren = (children: React.element) => { -
    -

    {React.string("Overview")}

    - children -
    -} - -wrapChildren(
    React.string("Let's use React with ReScript")
    ) -``` - -Understanding the definition of a `React.element` is essential since it is heavily used within the React APIs, such as `ReactDOM.render(element, ...)`, etc. Be aware that JSX doesn't do any automatic `string` to `React.element` conversion for you (ReScript forces explicit type conversion). For example `
    Hello World
    ` will not type-check (which is actually a good thing because it's also a huge source for subtle bugs!), you need to convert your `"Hello World"` with the `React.string` function first. - -Fortunately our React bindings bring all necessary functionality to represent all relevant data types as `React.element`s. - -## Creating Elements - -### Creating Elements from `string`, `int`, `float`, `array` - -Apart from using JSX to create our React elements or React components, the `React` module offers various functions to create elements from primitive data types: - -```res -React.string("Hello") // new element representing "Hello" - -React.int(1) // new element representing "1" - -React.float(1.0) // new element representing "1.0" -``` - -It also offers `React.array` to represent multiple elements as one single element (useful for rendering a list of data, or passing children): - -```res -let element = React.array([ - React.string("element 1"), - React.string("element 2"), - React.string("element 3") -]) -``` - -**Note:** We don't offer a `React.list` function because a `list` value would impose runtime overhead. ReasonReact cares about clean, idiomatic JS output. If you want to transform a `list` of elements to a single React element, combine the output of `Belt.List.toArray` with `React.array` instead. - -### Creating Null Elements - -ReScript doesn't allow `element || null` constraints due to it's strongly typed nature. Whenever you are expressing conditionals where a value might, or might not be rendered, you will need the `React.null` constant to represent *Nothingness*: - - - - -```res -let name = Some("Andrea") - -let element = switch name { - | Some(name) =>
    {React.string("Hello " ++ name)}
    - | None => React.null -} - -
    element
    -``` -```js -var name = "Andrea"; - -var element = name !== undefined ? React.createElement("div", undefined, "Hello " + name) : null; - -React.createElement("div", undefined, element); -``` +Unlike browser DOM elements, React elements are plain objects, and are cheap to create. React DOM takes care of updating the DOM to match the React elements. -
    - -### Creating Elements from Component Functions - -**Note:** Details on components and props will be described in the [next chapter](./components-and-props). - -Sometimes it's necessary to pass around component functions to have more control over `React.element` creation. Use the `React.createElement` function to instantiate your elements: - -```res -type props = {"name": string}; - -let render = (myComp: props => React.element) => { -
    - {React.createElement(myComp, {"name": "Franz"})} -
    -} -``` - -**Note:** This feature is often used when interacting with existing JS / ReactJS code. In pure ReScript React applications, you would rather pass a function that does the rendering for you (also called a "render prop"): - -```res -let render = (renderMyComp: (~name: string) => React.element) => { -
    - {renderMyComp("Franz")} -
    -} -``` - -#### Pass Variadic Children - -There is also a `React.createElementVariadic` function, which takes an array of children as a third parameter: - - - -```res -type props = {"title": string, "children": React.element}; - -let render = (article: props => React.element) => { - let children = [React.string("Introduction"), React.string("Body")]; - - let props = {"title": "Article #1", "children": React.null}; - - {React.createElementVariadic(article, props, children)} -} -``` -```js -function render(article) { - var children = ["Introduction"]; - var props = { - title: "Article #1", - children: null - }; - return Caml_splice_call.spliceApply(React.createElement, [ - article, - props, - children - ]); -} -``` - - - -**Note:** Here we are passing a prop `"children": React.null` to satisfy the type checker. React will ignore the children prop in favor of the children array. - -This function is mostly used by our JSX transformations, so usually you want to use `React.createElement` and pass a children prop instead. - -### Creating DOM Elements - -**Note:** The following examples showcase low level APIs that in most cases will only be useful for situations where JSX doesn't work. - -To create DOM elements (`
    `, ``, etc.), use `ReactDOMRe.createDOMElementVariadic`: - -```res -ReactDOMRe.createDOMElementVariadic("div", ~props=ReactDOM.domProps(~className="card", ()), []); -``` - -The function above requires the `ReactDOM.domProps` constructor function, so ReScript can make sure that we are only passing valid dom props. You can find an exhaustive list of all available props in the [ReactDOM](https://github.com/reasonml/reason-react/blob/master/src/ReactDOM.re#L61) module. - - -## Cloning Elements - -**Note:** This is an escape hatch feature and will only be useful for interoping with existing JS code / libraries. - -Sometimes it's required to clone an existing element to set, overwrite or add prop values to a new instance, or if you want to set invalid prop names such as `data-name`. You can use `React.cloneElement` for that: - - - -```res -let original =
    - -// Will return a new React.element with className set to "world" -React.cloneElement(original, {"className": "world", "data-name": "some name"}); -``` -```js -var original = React.createElement("div", { - className: "hello" - }); - -React.cloneElement(original, { - className: "world", - "data-name": "some name" - }); -``` - - - -The feature mentioned above could also replicate `props spreading`, a practise commonly used in ReactJS codebases, but we strongly discourage the usage due to its unsafe nature and its incorrectness (e.g. adding undefined extra props to a component doesn't make sense, and causes hard to find bugs). - -In ReScript, we rather pass down required props explicitly to leaf components or use a renderProp instead. We introduced [JSX punning](/docs/manual/latest/jsx#punning) syntax to make the process of passing down props more convenient. - -## Using Elements within JSX - -You can compose elements into more complex structures by using JSX: - -```res -let greeting = React.string("Hello ") -let name = React.string("Stranger"); +## Rendering Elements to the DOM +Let's assume we've got an HTML file that contains a `div` like this: -// element is also of type React.element -let element =
    greeting name
    +```html +
    ``` -JSX is the main way to express your React application as a tree of elements. We will discuss JSX specifics in more detail later on. +We call this a “root” DOM node because everything inside it will be managed by React DOM. -## Rendering Elements to the DOM +Plain React applications usually have a single root DOM node. If you are integrating React into an existing app, you may have as many isolated root DOM nodes as you like. -Now that we are able to create and manipulate a tree of React elements, it's time to tell React to render our actual DOM. +To render our React application into the `root` div, we need to do two things: +- Find our DOM node with `ReactDOM.querySelector` +- Render our React element to our queried `Dom.element` with `ReactDOM.render` -First you'd need to find a DOM node to mount your app to. For this, you can use the `ReactDOM.querySelector` utility function to find your dom node that can later be used by `ReactDOM.render` to render the React tree: +Here is a full example rendering our application in our `root` div: @@ -244,6 +59,8 @@ if (!(root == null)) { -**Note:** The code above expects a dom element in your e.g. `index.html`, e.g. something like this: `
    `. This is the parent element React will render your React code into. +React elements are immutable. Once you create an element, you can’t change its children or attributes. An element is like a single frame in a movie: it represents the UI at a certain point in time. +At this point we would need to understand a few more concepts, such as React components and state management to be able to update the rendered elements after the initial `ReactDOM.render`. For now, just imagine your React application as a tree of elements, whereby some elements may get replaced during the lifetime of your application. +React will automatically recognize any element changes and will only apply the DOM updates necessary to bring the DOM to the desired state. diff --git a/scripts/extract-tocs.js b/scripts/extract-tocs.js index ab02d7cb7..d77e8b83b 100644 --- a/scripts/extract-tocs.js +++ b/scripts/extract-tocs.js @@ -151,6 +151,7 @@ const createReactToc = () => { const FILE_ORDER = [ "introduction", "installation", + "elements-and-jsx", "rendering-elements", "components-and-props", "arrays-and-keys", From d38e19ea5a6f9c408ba46c8820a1bed2dba11248 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sun, 1 Nov 2020 12:50:47 +0100 Subject: [PATCH 21/29] Remove false statement in hooks-effect --- pages/docs/react/latest/hooks-effect.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/docs/react/latest/hooks-effect.mdx b/pages/docs/react/latest/hooks-effect.mdx index c95a30293..cf945e300 100644 --- a/pages/docs/react/latest/hooks-effect.mdx +++ b/pages/docs/react/latest/hooks-effect.mdx @@ -295,7 +295,7 @@ React.useEffect1(() => { **Important:** If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders -If you want to run an effect and clean it up only once (on mount and unmount), use `React.useEffect0`. This is essentially like comparing `[] === []`. +If you want to run an effect and clean it up only once (on mount and unmount), use `React.useEffect0`. If you are interested in more performance optmization related topics, have a look at the ReactJS [Performance Optimization Docs](https://reactjs.org/docs/hooks-faq.html#performance-optimizations) for more detailed infos. From 5d95a833372d6062c1d25b937c77467bfa8ecab6 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sun, 1 Nov 2020 12:51:32 +0100 Subject: [PATCH 22/29] Refactor forwardRef doc --- pages/docs/react/latest/forwarding-refs.mdx | 57 +++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/pages/docs/react/latest/forwarding-refs.mdx b/pages/docs/react/latest/forwarding-refs.mdx index 8137e7a8f..8a652eabc 100644 --- a/pages/docs/react/latest/forwarding-refs.mdx +++ b/pages/docs/react/latest/forwarding-refs.mdx @@ -13,7 +13,7 @@ Ref forwarding is a technique for automatically passing a [React.ref](./refs-and -## Forwarding Refs to DOM Components +## Why Ref Forwarding? Consider a FancyButton component that renders the native button DOM element: @@ -32,10 +32,58 @@ React components hide their implementation details, including their rendered out Although such encapsulation is desirable for application-level components like `FeedStory` or `Comment`, it can be inconvenient for highly reusable “leaf” components like `FancyButton` or `MyTextInput`. These components tend to be used throughout the application in a similar manner as a regular DOM button and input, and accessing their DOM nodes may be unavoidable for managing focus, selection, or animations. -**Ref forwarding is an opt-in feature that lets some components take a ref they receive, and pass it further down (in other words, “forward” it) to a child.** +There are currently two strategies on forwarding refs through a component. In ReScript and React we strongly recommend **passing your ref as a prop**, but there is also a dedicated API called `React.forwardRef`. -In the example below, `FancyInput` uses `React.forwardRef` to obtain the ref passed to it, and then forward it to the DOM input that it renders: +We will discuss both methods in this document. +## Forward Refs via Props + +A `React.ref` can be passed down like any other prop. The component will take care of forwarding the ref to the right DOM element. + +**No new concepts to learn!** + +In the example below, `FancyInput` defines a prop `inputRef` that will be forwarded to its `input` element: + +```res +// App.res + +module FancyInput = { + @react.component + let make = (~children, ~inputRef: ReactDOM.domRef) => +
    children
    +} + +@bs.send external focus: Dom.element => unit = "focus" + +@react.component +let make = () => { + let input = React.useRef(Js.Nullable.null) + + let focusInput = () => + input.current + ->Js.Nullable.toOption + ->Belt.Option.forEach(input => input->focus) + + let onClick = _ => focusInput() + +
    + + + +
    +} +``` + +We use the `ReactDOM.domRef` type to represent our `inputRef`. We pass our ref in the exact same manner as we'd do a DOM `ref` attribute (``). + + +## [Discouraged] React.forwardRef + +**Note:** We discourage this method since it [will likely go away](https://twitter.com/dan_abramov/status/1173372190395445251) at some point, and doesn't yield any obvious advantages over the previously mentioned ref prop passing. + +`React.forwardRef` offers a way to "emulate a `ref` prop" within custom components. Internally the component will forward the passed `ref` value to the target DOM element instead. + +This is how the previous example would look like with the `React.forwardRef` approach: @@ -132,5 +180,4 @@ This way, components using `FancyInput` can get a ref to the underlying `input` ## Note for Component Library Maintainers - -**When you start using forwardRef in a component library, you should treat it as a breaking change and release a new major version of your library**. This is because your library likely has an observably different behavior (such as what refs get assigned to, and what types are exported), and this can break apps and other libraries that depend on the old behavior. +**When you start using ref forwarding in a component library, you should treat it as a breaking change and release a new major version of your library**. This is because your library likely has an observably different behavior (such as what refs get assigned to, and what types are exported), and this can break apps and other libraries that depend on the old behavior. From 0b727e9e0cad147e7b0c525befd66d85c57a1154 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sun, 1 Nov 2020 12:51:40 +0100 Subject: [PATCH 23/29] Fix link in intro --- pages/docs/react/latest/introduction.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/docs/react/latest/introduction.mdx b/pages/docs/react/latest/introduction.mdx index 740d8ee5e..90fe97f6a 100644 --- a/pages/docs/react/latest/introduction.mdx +++ b/pages/docs/react/latest/introduction.mdx @@ -31,7 +31,7 @@ The React bindings are officially called **ReasonReact** due to legacy reasons. - Bindings for all important React APIs needed to build production ready apps (`useState`, `useReducer`, `useEffect`, `useRef`,...) - No class based component API legacy (all ReasonReact codebases are built on functional components & hooks) - Strong level of type safetiness and type inference for component props and state values -- [GenType](/docs/gentype/latest) support for importing / exporting React components in Flow and TypeScript codebases +- [GenType](/docs/gentype/latest/introduction) support for importing / exporting React components in Flow and TypeScript codebases ## Development From 2700073d4a7983c908d8d50e0a75b6cd0f707a25 Mon Sep 17 00:00:00 2001 From: Patrick Stapfer Date: Sun, 1 Nov 2020 13:15:41 +0100 Subject: [PATCH 24/29] Add context doc --- pages/docs/react/latest/context.mdx | 208 ++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 pages/docs/react/latest/context.mdx diff --git a/pages/docs/react/latest/context.mdx b/pages/docs/react/latest/context.mdx new file mode 100644 index 000000000..28c12db25 --- /dev/null +++ b/pages/docs/react/latest/context.mdx @@ -0,0 +1,208 @@ +--- +title: Context +description: "Details about Context in ReScript and React" +canonical: "/docs/react/latest/context" +category: "Main Concepts" +--- + +# Context + + + +Context provides a way to pass data through the component tree without having to pass props down manually at every level. + + + +## Why Context? + +In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree. + +**Note:** In ReScript, passing down props is way simpler than in TS / JS due to its [JSX prop punning](/docs/manual/latest/jsx#punning) feature and strong type inference, so it's often preferrable to keep it simple and just do props passing. Less magic means more transparency! + + +## When to Use Context + +Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language. For example, in the code below we manually thread through a “theme” prop in order to style the Button component: + + + +```res +// src/App.res +type theme = Light | Dark; + +module Button = { + @react.component + let make = (~theme) => { + let className = switch theme { + | Light => "theme-light" + | Dark => "theme-black" + }; + + } +} + +module ThemedButton = { + @react.component + let make = (~theme) => { + + } +} + +module ThemedButton = { + @react.component + let make = () => { + let theme = React.useContext(ThemeContext.context) + +