diff --git a/docs/callback-handlers.md b/docs/callback-handlers.md deleted file mode 100644 index 1c6633fec..000000000 --- a/docs/callback-handlers.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Callback Handlers ---- - - - -This section describes how ReactJS' `
` pattern translates into ReasonReact. - -## Callback Without State Update - -Two scenarios. - -### Without Reading From `self` - -_Reminder: `self` is ReasonReact's `this`. It's a record that contains things like `state`, `send` and others._ - -If you're just forwarding a callback prop onto your child, you'd do exactly the same thing you'd have done in ReactJS: - -```reason -let component = /* ... */; - -let make = (~name, ~onClick, _children) => { - ...component, - render: (self) => ; -``` - -If this kind of components need to expose a `Jsx2` module for backwards compatibility, like seen above, you might run into errors like: - -``` -This expression has type array('a) -but an expression was expected of type -ReasonReact.reactElement = React.element -``` - -In these cases, it is helpful to wrap `children` with `React.array` inside the `Jsx2` compatibility module, like: - -```reason -/* In SomeButton.re */ -module Jsx2 = { - let component = ReasonReact.statelessComponent("SomeButton"); - let make = children => { - let children = React.array(children); - ReasonReactCompat.wrapReactForReasonReact( - make, - makeProps(~children, ()), - children, - ); - }; -}; -``` - -The reason behind those errors is that version 3 of JSX doesn't automatically wrap the `children` passed to an element in an array, like version 2 used to do. Using `React.array` in the `Jsx2` module is a way to provide a consistent behavior for usages of the component in both versions of the JSX transform. - -## `wrapReasonReactForReact`: Wrapping a v2 component to be used from a v3 component - -Sometimes we might need to make the translation the other way around: wrap a v2 component to be used from a v3 component. For example, if we are using some ReasonReact library that has not been updated yet to be compatible with the latest version. - -In these cases, we can use `ReasonReact.wrapReasonReactForReact`. - -Let's say we have a v2 component `Text` that needs to be used from a v3 component `Heading`: - -```reason -/* In Text.re */ -let component = ReasonReact.statelessComponent("Text"); - -let make = (~text, _children) => { - ...component, - render: _self => {ReasonReact.string(text)} , -}; -``` - -We can follow the same approach as above and add a `Jsx3` module to the same file: - -```reason -/* Still in Text.re */ -module Jsx3 = { - [@bs.obj] external makeProps: (~text: string, unit) => _ = ""; - let make = - ReasonReactCompat.wrapReasonReactForReact( - ~component, (reactProps: {. "text": string}) => - make(~text=reactProps##text, [||]) - ); -}; -``` - -### Component with `children` - -If the v2 component we want to migrate uses `children` we have to take some extra steps to convert it. - -For example, let's say we have a `List` v2 component: - -```reason -/* List.re */ -let component = ReasonReact.statelessComponent("List"); - -let make = (~visible, children) => { - ...component, - render: _self => visible ?
...children
: ReasonReact.null, -}; -``` - -The `Jsx3` compat module will look like: - -```reason -/* Still in List.re */ -module Jsx3 = { - [@bs.obj] - external makeProps: (~visible: bool, ~children: 'children=?, unit) => _ = ""; - let make = - ReasonReactCompat.wrapReasonReactForReact( - ~component, - ( - reactProps: { - . - "visible": bool, - "children": 'children, - }, - ) => - make( - ~visible=reactProps##visible, - reactProps##children - ->Js.Undefined.toOption - ->Belt.Option.mapWithDefault([||], c => [|c|]), - ) - ); -}; -``` - -Because v3 components using `List.Jsx3` can decide whether to pass `children` or not, we have to account for those cases by setting `children` as optional labelled argument in `makeProps`. After that, we also have convert `children` to an array inside `make` before passing control over to the v2 implementation. - -## Migrating an application to v0.7.0 and JSX v3 - -There are many ReasonReact applications, so it is hard to define "The One True" migration strategy for them all. - -Depending on the size and nature of your application there are two options available to migrate your application from 0.6.0 to 0.7.0. - -### Application level - -By adding `{"reason": {"react-jsx": 3}` in your [`bsconfig.json`](https://bucklescript.github.io/docs/en/build-configuration.html#reason-refmt). - -This approach requires that all components in the application must be made compatible with version 3 of JSX at once, so it will be a better fit for smaller apps with a reduced number of components, where all of them can be migrated to version 3 in one fell swoop. - -### File level - -For larger applications, it might not be possible to migrate all components at once. In these cases, a per-file migration is also possible. - -A file can be configured to use version 3 of the transform by adding `[@bs.config {jsx: 3}];` at the top of the file. - -The per-file configuration allows to mix, in the same application, components compatible with either of both versions of the JSX transforms. However, the restriction is that all the components used in a file will have to be compatible with the JSX version specified for that file. - -For example, if a file contains the following code: - -```reason -/* User.re */ -[@bs.config {jsx: 3}]; - -[@react.component] -let make = (~id) => { - - - ; -}; -``` - -Then `Profile` and `UserDetails` components will have to be compatible with the version 3 of JSX. Or alternatively, if they are using version 2, they can be wrapped with the function `ReasonReactCompat.wrapReasonReactForReact`, as seen in [the section above](https://reasonml.github.io/reason-react/docs/en/reasonreactcompat.html#wrapreasonreactforreact-wrapping-a-v2-component-to-be-used-from-a-v3-component). - -#### From primitives to more complex components - -As all usages of any component in a file need to be migrated to the same version of JSX, one natural way to tackle large migrations at the file level is to start converting the most primitive components to version 3, as they generally render elements of a reduced number of components, or host elements like `
`. Once the most simple components are done, one can proceed with the most complex ones. - -Once all components are using version 3, there is no more need to keep the `[@bs.config {jsx: 3}];` annotations at the top of each file, and they can be replaced by bumping the JSX version in the `bsconfig.json` file to `{"reason": {"react-jsx": 3}` for the whole application. - -### Upgrade script - -A migration script [is provided](https://github.com/chenglou/upgrade-reason-react#installation) to facilitate the task to convert components to version 3. It will wrap existing ReasonReact components as if they are Hooks components. This script will not attempt to re-write your logic as hooks because this is often a complicated process and it is not guaranteed to be correct. Please always inspect and test the work of the migration script to make sure it does what you are expecting. diff --git a/docs/record-field-send-handle-not-found.md b/docs/record-field-send-handle-not-found.md deleted file mode 100644 index f232a1b05..000000000 --- a/docs/record-field-send-handle-not-found.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Record Field send/handle Not Found ---- - -Do you see a type error related to this? This might mean that you've passed `self` to a helper function of your render, and it used it like so: `
self.send(Click)} />`. This is because the record can't be found in the scope of the file. Just annotate it: `
self.ReasonReact.send(Click)} />`. - -More info [here](https://reasonml.github.io/docs/en/record.html#record-needs-an-explicit-definition). diff --git a/docs/render.md b/docs/render.md deleted file mode 100644 index 155aa9f47..000000000 --- a/docs/render.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Render ---- - - - -`render` needs to return a `ReasonReact.reactElement`: `
`, ``, etc. Render takes the argument `self`: - -```reason -/* ... */ - render: (self) =>
-/* ... */ -``` - -What if you want to return `null` from a `render`? Or pass a string to a DOM component like `div` which only allows `ReasonReact.reactElement`s? - -In ReactJS, you can easily do: `
hello
`, `
{1}
`, `
{null}
`, etc. In Reason, the type system restricts you from passing arbitrary data like so; you can only return `ReasonReact.reactElement` from `render`. - -Fortunately, we special-case a few special elements of the type `ReasonReact.reactElement`: - -- `ReasonReact.null`: This is your `null` equivalent for `render`'s return value. Akin to `return null` in ReactJS render. - -- `ReasonReact.string`: Takes a string and converts it to a `reactElement`. You'd use `
{ReasonReact.string(string_of_int(10))}
` to display an int. - -- `ReasonReact.array`: Takes an array and converts it to a `reactElement`. diff --git a/docs/router-2.md b/docs/router-2.md deleted file mode 100644 index f2ab5b559..000000000 --- a/docs/router-2.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: Router ---- - -ReasonReact comes with a router! We've leveraged the language and library features in order to create a router that's: - -- The simplest, thinnest possible. -- Easily pluggable anywhere into your existing code. -- Performant and tiny. - -[Here's the documented public interface](https://github.com/reasonml/reason-react/blob/main/src/ReasonReactRouter.rei), repeated here: - -- `ReasonReactRouter.push(string)`: takes a new path and update the URL. -- `ReasonReactRouter.replace(string)`: like `push`, but replaces the current URL. -- `ReasonReactRouter.watchUrl(f)`: start watching for URL changes. Returns a subscription token. Upon url change, calls the callback and passes it the `ReasonReactRouter.url` record. -- `ReasonReactRouter.unwatchUrl(watcherID)`: stop watching for url changes. -- `ReasonReactRouter.dangerouslyGetInitialUrl()`: get `url` record outside of `watchUrl`. Described later. -- `ReasonReactRouter.useUrl(~serverUrl)`: only usable in the new [function component api](https://reasonml.github.io/docs/en/components). - -## Match a Route - -**There's no API**! `watchUrl` gives you back a `url` record of the following shape: - -```reason -type url = { - /* path takes window.location.pathname, like "/book/title/edit" and turns it into `["book", "title", "edit"]` */ - path: list(string), - /* the url's hash, if any. The # symbol is stripped out for you */ - hash: string, - /* the url's query params, if any. The ? symbol is stripped out for you */ - search: string -}; -``` - - - -So the url `www.hello.com/book/10/edit?name=Jane#author` is given back as: - -```reason -{ - path: ["book", "10", "edit"], - hash: "author", - search: "name=Jane" -} -``` - -At this point, you can simply pattern match your way to glory! - -```reason -let watcherID = ReasonReact.Router.watchUrl(url => { - switch (url.path) { - | ["book", id, "edit"] => handleBookEdit(id) - | ["book", id] => getBook(id) - | ["book", id, _] => noSuchBookOperation() - | [] => showMainPage() - | ["shop"] | ["shop", "index"] => showShoppingPage() - | ["shop", ...rest] => - /* e.g. "shop/cart/10", but let "cart/10" be handled by another function */ - nestedMatch(rest) - | _ => showNotFoundPage() - } -}); - -/* some time later */ -ReasonReact.Router.unwatchUrl(watcherID); -``` - -So you can match a path, match a subset of a path, defer part of a matching to a nested logic, etc. - -### Tips & Tricks - -Notice that this is just normal [pattern matching](https://reasonml.github.io/docs/en/pattern-matching.html). You can combine it with other features, such as tuple + ReasonReact features like [subscriptions](subscriptions-helper.md) and reducer: - -```reason -let component = ReasonReact.reducerComponent("TodoApp"); - -let make = _children => { - ...component, - reducer: (action, state) => - switch (action) { - /* router actions */ - | ShowAll => ReasonReact.Update({...state, nowShowing: AllTodos}) - | ShowActive => /* ... */ - /* todo actions */ - | ChangeTodo(text) => /* ... */ - }, - didMount: self => { - let watcherID = ReasonReact.Router.watchUrl(url => { - switch (url.hash, MyAppStatus.isUserLoggedIn) { - | ("active", _) => self.send(ShowActive) - | ("completed", _) => self.send(ShowCompleted) - | ("shared", true) => self.send(ShowShared) - | ("shared", false) when isSpecialUser => /* handle this state please */ - | ("shared", false) => /* handle this state please */ - | _ => self.send(ShowAll) - } - }); - self.onUnmount(() => ReasonReact.Router.unwatchUrl(watcherID)); - }, - render: ... -} -``` - -## Directly Get a Route - -In one specific occasion, you might want to take hold of a `url` record _outside_ of `watchUrl`. For example, if you've put `watchUrl` inside a component's `didMount` so that a URL change triggers a component state change, you might also want the initial state to be dictated by the URL. - -In other words, you'd like to read from the `url` record once at the beginning of your app logic. We expose `dangerouslyGetInitialUrl()` for this purpose. - -**Note**: the reason why we label it as "dangerous" is to remind you **not** to read this `url` in any arbitrary component's e.g. `render`, since that information might be out of date if said component doesn't also contain a `watchUrl` subscription that re-renders the component when the URL changes. Aka, please only use `dangerouslyGetInitialUrl` alongside `watchUrl`. - -## Push a New Route - -From anywhere in your app, just call e.g. `ReasonReact.Router.push("/books/10/edit#validated")`. This will trigger a URL change (without a page refresh) and `watchUrl`'s callback will be called again. - -We might provide better facilities for typed routing + payload carrying in the future! - -**Note**: because of browser limitations, changing the URL through JavaScript (aka `pushState`) **cannot** be detected. The solution is to change the URL then fire a `"popState"` event. This is what `Router.push` does, and what the event `watchUrl` listens to. So if, for whatever reason (e.g. incremental migration), you want to update the URL outside of `Router.push`, just do `window.dispatchEvent(new Event('popState'))`. - -## Design Decisions - -We always strive to lower the performance and learning overhead in ReasonReact, and our router design's no different. The entire implementation, barring browser features detection, is around 20 lines. The design might seem obvious in retrospect, but to arrive here, we had to dig back into ReactJS internals & future proposals to make sure we understood the state update mechanisms, the future context proposal, lifecycle ordering, etc. and reject some bad API designs along the way. It's nice to arrive at such an obvious solution! - -The API also doesn't dictate whether matching on a route should return a component, a state update, or a side-effect. Flexible enough to slip into existing apps. - -Performance-wise, a JavaScript-like API tends to use a JS object of route string -> callback. We eschewed that in favor of pattern-matching, since the latter in Reason does not allocate memory, and is compiled to a fast jump table in C++ (through the JS JIT). In fact, the only allocation in the router matching is the creation of the `url` record! diff --git a/docs/send-handle-callbacks-having-incompatible-types.md b/docs/send-handle-callbacks-having-incompatible-types.md deleted file mode 100644 index 5fb62d0f4..000000000 --- a/docs/send-handle-callbacks-having-incompatible-types.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: send/handle callbacks having Incompatible Types ---- - -You've probably passed `self.send` to a helper function that uses this `send` reference twice. For complex reasons this doesn't type; you'd have to pass in the whole `self` to the helper. diff --git a/docs/state-actions-reducer.md b/docs/state-actions-reducer.md deleted file mode 100644 index 840c995aa..000000000 --- a/docs/state-actions-reducer.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: State, Actions & Reducer ---- - - - -Finally, we're getting onto stateful components! - -ReasonReact stateful components are like ReactJS stateful components, except with the concept of "reducer" (like [Redux](http://redux.js.org)) built in. If that word doesn't mean anything to you, just think of it as a state machine. If _that_ word does mean something to you, just think: "Woah this is great". - -To declare a stateful ReasonReact component, instead of `ReasonReact.statelessComponent("MyComponentName")`, use `ReasonReact.reducerComponent("MyComponentName")`. - -Here's a complete, working, stateful ReasonReact component. We'll refer to it later on. - -```reason -/* State declaration */ -type state = { - count: int, - show: bool, -}; - -/* Action declaration */ -type action = - | Click - | Toggle; - -/* Component template declaration. - Needs to be **after** state and action declarations! */ -let component = ReasonReact.reducerComponent("Example"); - -/* greeting and children are props. `children` isn't used, therefore ignored. - We ignore it by prepending it with an underscore */ -let make = (~greeting, _children) => { - /* spread the other default fields of component here and override a few */ - ...component, - - initialState: () => {count: 0, show: true}, - - /* State transitions */ - reducer: (action, state) => - switch (action) { - | Click => ReasonReact.Update({...state, count: state.count + 1}) - | Toggle => ReasonReact.Update({...state, show: !state.show}) - }, - - render: self => { - let message = - "You've clicked this " ++ string_of_int(self.state.count) ++ " times(s)"; -
- - - ( - self.state.show - ? ReasonReact.string(greeting) - : ReasonReact.null - ) -
; - }, -}; -``` - -## `initialState` - -ReactJS' `getInitialState` is called `initialState` in ReasonReact. It takes `unit` and returns the state type. The state type could be anything! An int, a string, a ref or the common record type, which you should declare **right before the `reducerComponent` call**: - -```reason -type state = {count: int, show: bool}; - -let component = ReasonReact.reducerComponent("Example"); - -let make = (~onClick, _children) => { - ...component, - initialState: () => {count: 0, show: true}, - /* ... other fields */ -}; - -``` - -Since the props are just the arguments on `make`, feel free to read into them to initialize your state based on them. - -## Actions & Reducer - -In ReactJS, you'd update the state inside a callback handler, e.g. - -```javascript -{ - /* ... other fields */ - handleClick: function() { - this.setState({count: this.state.count + 1}); - }, - handleSubmit: function() { - this.setState(...); - }, - render: function() { - return ( - - ); - } -} -``` - -In ReasonReact, you'd gather all these state-setting handlers into a single place, the component's `reducer`! **Please refer to the first snippet of code on this page**. - -**Note**: if you ever see mentions of `self.reduce`, this is the old API. The new API is called `self.send`. The old API's docs are [here](https://github.com/reasonml/reason-react/blob/e17fcb5d27a2b7fb2cfdc09d46f0b4cf765e50e4/docs/state-actions-reducer.md). - -A few things: - -- There's a user-defined type called **`action`**, named so by convention. It's a variant of all the possible state transitions in your component. _In state machine terminology, this'd be a "token"_. -- A user-defined `state` type, and an `initialState`. Nothing special. -- The current `state` value is accessible through `self.state`, whenever `self` is passed to you as an argument of some function. -- A "**reducer**"! This [pattern-matches](https://reasonml.github.io/docs/en/pattern-matching.html) on the possible actions and specifies what state update each action corresponds to. _In state machine terminology, this'd be a "state transition"_. -- In `render`, instead of `self.handle` (which doesn't allow state updates), you'd use `self.send`. `send` takes an action. - -So, when a click on the dialog is triggered, we "send" the `Click` action to the reducer, which handles the `Click` case by returning the new state that increments a counter. ReasonReact takes the state and updates the component. - -**Note**: just like for `self.handle`, sometimes you might be forwarding `send` to some helper functions. Pass the whole `self` instead and **annotate it**. This avoids a complex `self` record type behavior. See [Record Field `send`/`handle` Not Found](record-field-send-handle-not-found.md). - -## State Update Through Reducer - -Notice the return value of `reducer`? The `ReasonReact.Update` part. Instead of returning a bare new state, we ask you to return the state wrapped in this "update" variant. Here are its possible values: - -- `ReasonReact.NoUpdate`: don't do a state update. -- `ReasonReact.Update(state)`: update the state. -- `ReasonReact.SideEffects(self => unit)`: no state update, but trigger a side-effect, e.g. `ReasonReact.SideEffects(_self => Js.log("hello!"))`. -- `ReasonReact.UpdateWithSideEffects(state, self => unit)`: update the state, **then** trigger a side-effect. - -### Important Notes - -**Please read through all these points**, if you want to fully take advantage of `reducer` and avoid future ReactJS Fiber race condition problems. - -- The `action` type's variants can carry a payload: `onClick=(data => self.send(Click(data.foo)))`. -- Don't pass the whole event into the action variant's payload. ReactJS events are pooled; by the time you intercept the action in the `reducer`, the event's already recycled. -- `reducer` **must** be pure! Aka don't do side-effects in them directly. You'll thank us when we enable the upcoming concurrent React (Fiber). Use `SideEffects` or `UpdateWithSideEffects` to enqueue a side-effect. The side-effect (the callback) will be executed after the state setting, but before the next render. -- If you need to do e.g. `ReactEvent.BlablaEvent.preventDefault(event)`, do it in `self.send`, before returning the action type. Again, `reducer` must be pure. -- Feel free to trigger another action in `SideEffects` and `UpdateWithSideEffects`, e.g. `UpdateWithSideEffects(newState, (self) => self.send(Click))`. -- If your state only holds instance variables, it also means (by the convention in the instance variables section) that your component only contains `self.handle`, no `self.send`. You still need to specify a `reducer` like so: `reducer: ((), _state) => ReasonReact.NoUpdate`. Otherwise you'll get a `variable cannot be generalized` type error. - -### Tip - -Cram as much as possible into `reducer`. Keep your actual callback handlers (the `self.send(Foo)` part) dumb and small. This makes all your state updates & side-effects (which itself should mostly only be inside `ReasonReact.SideEffects` and `ReasonReact.UpdateWithSideEffects`) much easier to scan through. Also more ReactJS fiber async-mode resilient. - -## Async State Setting - -In ReactJS, you could use `setState` inside a callback, like so: - -``` -setInterval(() => this.setState(...), 1000); -``` - -In ReasonReact, you'd do something similar: - -```reason -Js.Global.setInterval(() => self.send(Tick), 1000) -``` diff --git a/docs/subscriptions-helper.md b/docs/subscriptions-helper.md deleted file mode 100644 index 9c69fb520..000000000 --- a/docs/subscriptions-helper.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Subscriptions Helper ---- - -In a large, heterogeneous app, you might often have legacy or interop data sources that come from outside of the React/ReasonReact tree, or a timer, or some browser event handling. You'd listen and react to these changes by, say, updating the state. - -For example, Here's what you're probably doing currently, for setting up a timer event: - -```reason -type state = { - timerId: ref(option(Js.Global.intervalId)) -}; - -let component = ReasonReact.reducerComponent("Todo"); - -let make = _children => { - ...component, - initialState: () => {timerId: ref(None)}, - didMount: self => { - self.state.timerId := Some(Js.Global.setInterval(() => Js.log("hello!"), 1000)); - }, - willUnmount: self => { - switch (self.state.timerId^) { - | Some(id) => Js.Global.clearInterval(id); - | None => () - } - }, - render: /* ... */ -}; -``` - -Notice a few things: - -- This is rather boilerplate-y. -- Did you use a `ref(option(foo))` type correctly instead of a mutable field, as indicated by the [Instance Variables section](instance-variables.md)? -- Did you remember to free your timer subscription in `willUnmount`? - -For the last point, go search your codebase and see how many `setInterval` you have compared to the amount of `clearInterval`! We bet the ratio isn't 1 =). Likewise for `addEventListener` vs `removeEventListener`. - -To solve the above problems and to codify a good practice, ReasonReact provides a helper field in `self`, called `onUnmount`. It asks for a callback of type `unit => unit` in which you free your subscriptions. It'll be called before the component unmounts. - -Here's the previous example rewritten: - -```reason -let component = ReasonReact.statelessComponent("Todo"); - -let make = _children => { - ...component, - didMount: self => { - let intervalId = Js.Global.setInterval(() => Js.log("hello!"), 1000); - self.onUnmount(() => Js.Global.clearInterval(intervalId)); - }, - render: /* ... */ -}; -``` - -Now you won't ever forget to clear your timer! - -## Design Decisions - -**Why not just put some logic in the willUnmount `lifecycle`**? Definitely do, whenever you could. But sometimes, folks forget to release their subscriptions inside callbacks: - -```reason -let make = _children => { - ...component, - reducer: (action, state) => { - switch (action) { - | Click => ReasonReact.SideEffects(self => { - fetchAsyncData(result => { - self.send(Data(result)) - }); - }) - } - }, - render: /* ... */ -}; -``` - -If the component unmounts and _then_ the `fetchAsyncData` calls the callback, it'll accidentally call `self.send`. Using `let cancel = fetchAsyncData(...); self.onUnmount(() => cancel())` is much easier. - -**Note**: this is an **interop helper**. This isn't meant to be used as a shiny first-class feature for e.g. adding more flux stores into your app (for that purpose, please use our [local reducer](state-actions-reducer.md#actions-reducer)). Every time you use `self.onUnmount`, consider it as a simple, pragmatic and performant way to talk to the existing world. diff --git a/docs/tailwind-css.md b/docs/tailwind-css.md index 5e86e402b..38416b136 100644 --- a/docs/tailwind-css.md +++ b/docs/tailwind-css.md @@ -2,7 +2,7 @@ title: Styling: Tailwind CSS --- -[Tailwind CSS](https://tailwindcss.com) is a new CSS framework that is rapidly +[Tailwind CSS](https://tailwindcss.com) is a CSS framework that is rapidly growing in popularity. It's completely customizable and lightweight, making it a perfect companion to React. If you're not familiar with Tailwind, we recommend checking out [their docs](https://tailwindcss.com/#what-is-tailwind) for @@ -40,43 +40,3 @@ let make = () => which gives us the following UI: - -## tailwind-ppx - -Often times when you're writing with Tailwind and ReasonReact, you may find -yourself wondering why the UI isn't working like it should, only to find out you -had a typo in a class name. For example, - -```reason -
- ... -
-``` - -Wouldn't it be nice to get some validation _while_ you're writing the Tailwind -classes? Better yet, how about preventing your code from even compiling if the -classes aren't correct? Well, enter -[`tailwind-ppx`](https://github.com/dylanirlbeck/tailwind-ppx): a compile-time -validator for Tailwind CSS. Using this PPX, you can get immediate compiler -errors if you happen to misspell class names, along with a nice suggestion of -what you may have meant to write! - -```reason -
/* ERROR: Class name not found: flex-ro. Did you mean flex-row? */ - ... -
-``` - -Moreover, in a large codebase where components may have many class names, you -may find yourself duplicating some class names. `tailwind-ppx` solves this -issue, too! - -```reason -
/* ERROR: Duplicate class name: flex */ - ... -
-``` - -Wrapping the class names in a PPX allows for some powerful integrations with -Tailwind in addition to validation. For more information, check out -`tailwind-ppx`'s [other features](https://github.com/dylanirlbeck/tailwind-ppx#features) diff --git a/docs/testing.md b/docs/testing.md index 73b753784..ae591d9ba 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -2,6 +2,8 @@ title: Testing ReasonReact components --- + + Even though types make your code safer, it doesn't remove the need for testing! If you want to test your ReasonReact components using your JavaScript testing stack, that's perfectly alright, but if you prefer to write them in Reason, here are some testing frameworks and bindings to make your life easier: diff --git a/docs/use-state-use-effect.md b/docs/use-state-use-effect.md deleted file mode 100644 index 4fc9d9fd5..000000000 --- a/docs/use-state-use-effect.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: useState, useEffect in a Form ---- - -Here's a simple example of how to use React's `useState` with `useEffects`. - -```reason -[@react.component] -let make = (~label, ~onSubmit) => { - let (editing, setEditing) = React.useState(() => false); - let (value, onChange) = React.useState(() => label); - let onCancel = _evt => setEditing(_ => false); - let onFocus = event => ReactEvent.Focus.target(event)##select(); - - React.useEffect1( - () => { - onChange(_ => label); - None - }, - [|label|], - ); - - if (editing) { -
{ - setEditing(_ => false); - onSubmit(value); - }} - onBlur=onCancel> - { - let value = ReactEvent.Form.target(event)##value; - onChange(_ => value) - } - } - value - /> -
; - } else { - setEditing(_ => true)}> - value->React.string - ; - }; -}; -``` diff --git a/docs/useeffect-hook.md b/docs/useeffect-hook.md index 395941246..ad07f7907 100644 --- a/docs/useeffect-hook.md +++ b/docs/useeffect-hook.md @@ -1,5 +1,5 @@ --- -title: useEffect Hook +title: useEffect --- [React.js docs for useEffect](https://reactjs.org/docs/hooks-reference.html#useeffect) diff --git a/docs/usereducer-hook.md b/docs/usereducer-hook.md index 177e76f35..f9d4ae13b 100644 --- a/docs/usereducer-hook.md +++ b/docs/usereducer-hook.md @@ -1,37 +1,36 @@ --- -title: useReducer Hook +title: useReducer --- [React.js docs for useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer) ->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. +> 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. ```reason -/* we create a type for the action */ +/* we create a type for the action, but action can be anything */ type action = - | Tick; + | Increment + | Decrement; -/* and do the same for state */ -type state = {count: int}; +/* similarly on 'state', it can be anything. In this case, it's an int */ +let reducer = (state, action) => + switch (action) { + | Increment => state + 1 + | Decrement => state - 1 + }; [@react.component] -let make = () => { - let (state, dispatch) = - React.useReducer( - (state, action) => - switch (action) { - | Tick => {count: state.count + 1} - }, - {count: 0}, - ); + let make = (~initialValue=0) => { + let (state, dispatch) = React.useReducer(reducer, initialValue); - /* useEffect hook takes 0 arguments hence, useEffect0 */ - React.useEffect0(() => { - let timerId = Js.Global.setInterval(() => dispatch(Tick), 1000); - Some(() => Js.Global.clearInterval(timerId)); - }); - - /* ints need to be converted to strings, that are then consumed by React.string */ -
{React.string(string_of_int(state.count))}
; -}; +
+
state->React.int
+ + +
; + }; ``` diff --git a/docs/usestate-event-value.md b/docs/usestate-event-value.md index e3fae88f4..bb71d18ed 100644 --- a/docs/usestate-event-value.md +++ b/docs/usestate-event-value.md @@ -31,7 +31,7 @@ let make = () => { setName(_ => ReactEvent.Form.target(event)##value) + onChange={event => setName(_ => React.Event.Form.target(event)##value) />; }; ``` @@ -62,7 +62,7 @@ let make = () => { value={name} onChange={ event => { - let value = ReactEvent.Form.target(event)##value; + let value = React.Event.Form.target(event)##value; setName(_ => value) } } diff --git a/docs/usestate-hook.md b/docs/usestate-hook.md index 7a1f68272..167f9b2d0 100644 --- a/docs/usestate-hook.md +++ b/docs/usestate-hook.md @@ -1,25 +1,30 @@ --- -title: useState Hook +title: useState --- +The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component. + [React.js docs for useState](https://reactjs.org/docs/hooks-reference.html#usestate) ->The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component. +### Usage -### A Simple Counter Example +The `useState` hook takes a function that returns the initial state value and returns a tuple with the current state value and a function to update the state. + +ReasonReact exposes the `useState` hook with the initialiser function, not with the inmediate value. ```reason -type action = - | Tick; +let useState: (unit => 'state) => ('state, 'state => unit); +``` -type state = {count: int}; +### A Simple Counter Example +```reason [@react.component] let make = (~initialCount) => { let (count, setCount) = React.useState(_ => initialCount); - <> - {React.string("Count: " ++ string_of_int(count))} +
+ {React.string("Count: " ++ Int.to_string(count))} @@ -29,6 +34,80 @@ let make = (~initialCount) => { - ; +
; }; ``` + +## Using Event values with useState + +In ReactJS, it's common to update a component's state based on an event's +value. Because ReasonReact's `useState` is slightly different than ReactJS, +directly translating JavaScript components to Reason can lead to a common bug. + +```js +/* App.js */ +function App() { + const [name, setName] = React.useState("John"); + return ( + setName(event.target.value)} + /> + ); +} +``` + +If we convert this component to reason, we may attempt to write this: + +```reason +/* App.re */ +/* WRONG! Don't do this! */ +[@react.component] +let make = () => { + let (name, setName) = React.useState(() => "John"); + setName(_ => React.Event.Form.target(event)##value) + />; +}; +``` + +Can you spot the bug? + +In the Reason version, the `onChange` callback won't correctly update the state. +This happens because the callback passed to `setName` is run *after* the `event` +variable is cleaned up by React, so the `value` field won't exist when it's +needed. + +This isn't actually any different than how events and `useState` hooks work in +ReactJS when you choose to use a callback with `setName`. The only difference +is that ReasonReact enforces that we always use callbacks. + +## Solution + +Fortunately, fixing this bug is simple: + +```reason +/* App.re */ +/* CORRECT! This code is bug-free. 👍 */ +[@react.component] +let make = () => { + let (name, setName) = React.useState(() => "John"); + { + let value = React.Event.Form.target(event)##value; + setName(_ => value) + } + } + />; +}; +``` + +The key is to extract the `value` from the `event` *before* we send it to +`setName`. Even if React cleans up the event, we don't lose access to the +value we need. diff --git a/docs/working-with-optional-data.md b/docs/working-with-optional-data.md index c6c40874b..2929c0c0d 100644 --- a/docs/working-with-optional-data.md +++ b/docs/working-with-optional-data.md @@ -2,7 +2,11 @@ title: Working with Optional Data --- -If you're coming from Javascript, optional data can be a real pain in the butt. ReasonML removes a *whole class* of `null` and `undefined` bugs which makes your code WAY safer and easier to write, but it takes some good examples to get you there :smile: +If you're coming from JavaScript, optional data can be a real pain in the butt. ReasonML removes a *whole class* of `null` and `undefined` bugs which makes your code safer and easier to write, but it takes some good examples to get you there :smile: + +ReasonML uses the `option` type to represent optional data. As defined in the standard library [here](https://reasonml.github.io/api/Option.html). + +Here there are a few examples of how to work with optional data in ReasonML, using the [Belt](https://melange.re/v4.0.0/api/re/melange/Belt) library from `melange.belt`. ### Accessing Optional Nested Data diff --git a/src/React.rei b/src/React.rei index c7664b2ba..2b3fe44e3 100644 --- a/src/React.rei +++ b/src/React.rei @@ -565,7 +565,8 @@ module Uncurried: { }; [@mel.module "react"] -external startTransition: ([@mel.uncurry] (unit => unit)) => unit = "startTransition"; +external startTransition: ([@mel.uncurry] (unit => unit)) => unit = + "startTransition"; [@mel.module "react"] external useTransition: unit => (bool, callback(callback(unit, unit), unit)) = @@ -591,11 +592,11 @@ module Event: { the generic synthetic event. The rest are the specific ones. In each module, the type `t` commonly means "the type of that module" (OCaml convention). In our case, e.g. - `ReactEvent.Mouse.t` represents a ReactJS synthetic mouse event. You'd use it to type your props: + `React.Event.Mouse.t` represents a ReactJS synthetic mouse event. You'd use it to type your props: ``` type props = { - onClick: ReactEvent.Mouse.t => unit + onClick: React.Event.Mouse.t => unit }; ``` @@ -607,13 +608,13 @@ module Event: { ``` let handleClick = ({state, props}, event) => { - ReactEvent.Mouse.preventDefault(event); + React.Event.Mouse.preventDefault(event); ... }; let handleSubmit = ({state, props}, event) => { /* this handler can be triggered by either a Keyboard or a Mouse event; conveniently use the generic preventDefault */ - ReactEvent.Synthetic.preventDefault(event); + React.Event.Synthetic.preventDefault(event); ... }; @@ -622,8 +623,8 @@ module Event: { How to translate idioms from ReactJS: - 1. myMouseEvent.preventDefault() -> ReactEvent.Mouse.preventDefault(myMouseEvent) - 2. myKeyboardEvent.which -> ReactEvent.Keyboard.which(myKeyboardEvent) + 1. myMouseEvent.preventDefault() -> React.Event.Mouse.preventDefault(myMouseEvent) + 2. myKeyboardEvent.which -> React.Event.Keyboard.which(myKeyboardEvent) */ type synthetic('a); diff --git a/src/ReactDOMServer.rei b/src/ReactDOMServer.rei index b967a1465..95a1cd02d 100644 --- a/src/ReactDOMServer.rei +++ b/src/ReactDOMServer.rei @@ -4,5 +4,3 @@ external renderToString: React.element => string = "renderToString"; [@mel.module "react-dom/server"] external renderToStaticMarkup: React.element => string = "renderToStaticMarkup"; - - diff --git a/website/i18n/en.json b/website/i18n/en.json index de9266ba9..51a3782a8 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -8,12 +8,6 @@ "adding-data-props": { "title": "Adding data-* attributes" }, - "callback-handlers": { - "title": "Callback Handlers" - }, - "children": { - "title": "Children" - }, "clone-element": { "title": "cloneElement" }, @@ -26,14 +20,8 @@ "components": { "title": "Components" }, - "context-mixins": { - "title": "Context & Mixins" - }, - "creation-props-self": { - "title": "Creation, Props & Self" - }, - "custom-class-component-property": { - "title": "Custom Class/Component Property" + "context": { + "title": "Context" }, "dom": { "title": "Working with DOM" @@ -50,12 +38,6 @@ "example-projects": { "title": "Example Projects" }, - "functional-component": { - "title": "Functional Component" - }, - "gentype": { - "title": "Gentype & Typescript" - }, "graphql-apollo": { "title": "GraphQL & Apollo" }, @@ -77,12 +59,6 @@ "installation": { "title": "Installation" }, - "instance-variables": { - "title": "Instance Variables" - }, - "interop": { - "title": "Talk to Existing ReactJS Code" - }, "intro-example": { "title": "Intro Example" }, @@ -92,66 +68,33 @@ "js-using-reason": { "title": "ReactJS using ReasonReact" }, - "jsx-2": { - "title": "JSX (Old, Version 2)" - }, "jsx": { "title": "JSX" }, - "lifecycles": { - "title": "Lifecycles" - }, "playground": { "title": "Playground" }, "props-spread": { "title": "Props Spread" }, - "react-ref": { - "title": "React Ref" - }, "reason-using-js": { "title": "ReasonReact using ReactJS" }, - "reasonreactcompat": { - "title": "ReasonReactCompat: migrating to 0.7.0 and JSX v3" - }, - "record-field-send-handle-not-found": { - "title": "Record Field send/handle Not Found" - }, "refs": { "title": "Refs in React" }, "render-props": { "title": "Render Props" }, - "render": { - "title": "Render" - }, - "roadmap": { - "title": "Roadmap & Contribution" - }, - "router-2": { - "title": "Router" - }, "router": { "title": "Router" }, - "send-handle-callbacks-having-incompatible-types": { - "title": "send/handle callbacks having Incompatible Types" - }, "simple": { "title": "A List of Simple Examples" }, - "state-actions-reducer": { - "title": "State, Actions & Reducer" - }, "style": { "title": "Style" }, - "subscriptions-helper": { - "title": "Subscriptions Helper" - }, "tailwind-css": { "title": "Styling: Tailwind CSS" }, @@ -165,19 +108,19 @@ "title": "useState, useEffect in a Form" }, "usedebounce-custom-hook": { - "title": "A Custom useDebounce Hook" + "title": "Custom Hooks" }, "useeffect-hook": { - "title": "useEffect Hook" + "title": "useEffect" }, "usereducer-hook": { - "title": "useReducer Hook" + "title": "useReducer" }, "usestate-event-value": { "title": "Using Event Values With useState" }, "usestate-hook": { - "title": "useState Hook" + "title": "useState" }, "what-and-why": { "title": "What & Why" @@ -197,6 +140,7 @@ "categories": { "Getting Started": "Getting Started", "Core": "Core", + "Hooks": "Hooks", "ReactJS Idioms Equivalents": "ReactJS Idioms Equivalents", "FAQ": "FAQ", "Recipes & Snippets": "Recipes & Snippets", diff --git a/website/sidebars.json b/website/sidebars.json index 5b6c6ca45..39167ff36 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -15,14 +15,19 @@ "refs", "testing" ], + "Hooks": [ + "usestate-hook", + "usereducer-hook", + "useeffect-hook", + "custom-hooks" + ], "ReactJS Idioms Equivalents": [ "invalid-prop-name", "props-spread", "component-as-prop", "ternary-shortcut", - "context-mixins", + "context", "custom-class-component-property", - "usestate-event-value", "error-boundaries" ], "FAQ": [ @@ -30,18 +35,12 @@ "record-field-send-handle-not-found", "send-handle-callbacks-having-incompatible-types", "i-really-need-feature-x-from-reactjs", - "element-type-is-invalid", "i-want-to-create-a-dom-element-without-jsx" ] }, "examples": { "Recipes & Snippets": [ "simple", - "usestate-hook", - "usereducer-hook", - "useeffect-hook", - "use-state-use-effect", - "usedebounce-custom-hook", "adding-data-props", "working-with-optional-data", "render-props", diff --git a/website/siteConfig.js b/website/siteConfig.js index 4ff84e339..b7a910a03 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -1,169 +1,169 @@ const users = [ - { - caption: "Facebook", - image: "img/logos/facebook.svg", - infoLink: "https://www.facebook.com", - pinned: true, - }, - { - caption: "Messenger", - image: "img/logos/messenger.svg", - infoLink: "https://messenger.com", - pinned: true, - }, - { - caption: "BeOp", - image: "img/logos/beop.svg", - infoLink: "https://beop.io", - pinned: true, - }, - { - caption: "Social Tables", - image: "img/logos/socialtables.svg", - infoLink: "https://www.socialtables.com", - pinned: true, - }, - { - caption: "Ahrefs", - image: "img/logos/ahrefs.svg", - infoLink: "https://ahrefs.com", - pinned: true, - }, - { - caption: "Astrocoders", - image: "img/logos/astrocoders.svg", - infoLink: "https://astrocoders.com", - pinned: true, - }, - { - caption: "Appier", - image: "img/logos/appier.svg", - infoLink: "https://appier.com", - pinned: true, - }, - { - caption: "Imandra Inc.", - image: "img/logos/imandra.svg", - infoLink: "https://www.imandra.ai", - pinned: true, - }, - { - caption: "Literal", - image: "img/logos/literal.svg", - infoLink: "https://literal.io", - pinned: true, - }, - { - caption: "Online Teaching Platform & LMS | PupilFirst", - image: "img/logos/pupilfirst.svg", - infoLink: "https://pupilfirst.com", - pinned: true, - }, - { - caption: "Atvero DMS", - image: "img/logos/atvero.svg", - infoLink: "https://www.atvero.com", - pinned: true, - }, - { - caption: "codeheroes", - image: "img/logos/codeheroes.svg", - infoLink: "https://codeheroes.io/", - }, - { - caption: "Astrolabe Diagnostics", - image: "img/logos/astrolabe.svg", - infoLink: "https://astrolabediagnostics.com/", - }, - { - caption: "Auditless", - image: "img/logos/auditless.svg", - infoLink: "https://auditless.com/", - pinned: true, - }, - { - caption: "Porter", - image: "img/logos/porter.svg", - infoLink: "https://porter.in/", - pinned: true, - }, - { - caption: "Lukin Co.", - image: "img/logos/lukin.svg", - infoLink: "https://lukin.co", - pinned: true, - }, - { - caption: "Greenlabs", - image: "img/logos/greenlabs.svg", - infoLink: "https://greenlabs.co.kr", - pinned: true, - }, + { + caption: "Facebook", + image: "img/logos/facebook.svg", + infoLink: "https://www.facebook.com", + pinned: true, + }, + { + caption: "Messenger", + image: "img/logos/messenger.svg", + infoLink: "https://messenger.com", + pinned: true, + }, + { + caption: "BeOp", + image: "img/logos/beop.svg", + infoLink: "https://beop.io", + pinned: true, + }, + { + caption: "Social Tables", + image: "img/logos/socialtables.svg", + infoLink: "https://www.socialtables.com", + pinned: true, + }, + { + caption: "Ahrefs", + image: "img/logos/ahrefs.svg", + infoLink: "https://ahrefs.com", + pinned: true, + }, + { + caption: "Astrocoders", + image: "img/logos/astrocoders.svg", + infoLink: "https://astrocoders.com", + pinned: true, + }, + { + caption: "Appier", + image: "img/logos/appier.svg", + infoLink: "https://appier.com", + pinned: true, + }, + { + caption: "Imandra Inc.", + image: "img/logos/imandra.svg", + infoLink: "https://www.imandra.ai", + pinned: true, + }, + { + caption: "Literal", + image: "img/logos/literal.svg", + infoLink: "https://literal.io", + pinned: true, + }, + { + caption: "Online Teaching Platform & LMS | PupilFirst", + image: "img/logos/pupilfirst.svg", + infoLink: "https://pupilfirst.com", + pinned: true, + }, + { + caption: "Atvero DMS", + image: "img/logos/atvero.svg", + infoLink: "https://www.atvero.com", + pinned: true, + }, + { + caption: "codeheroes", + image: "img/logos/codeheroes.svg", + infoLink: "https://codeheroes.io/", + }, + { + caption: "Astrolabe Diagnostics", + image: "img/logos/astrolabe.svg", + infoLink: "https://astrolabediagnostics.com/", + }, + { + caption: "Auditless", + image: "img/logos/auditless.svg", + infoLink: "https://auditless.com/", + pinned: true, + }, + { + caption: "Porter", + image: "img/logos/porter.svg", + infoLink: "https://porter.in/", + pinned: true, + }, + { + caption: "Lukin Co.", + image: "img/logos/lukin.svg", + infoLink: "https://lukin.co", + pinned: true, + }, + { + caption: "Greenlabs", + image: "img/logos/greenlabs.svg", + infoLink: "https://greenlabs.co.kr", + pinned: true, + }, ]; const examples = [ - { - name: "Hacker News", - image: "img/examples/hn.png", - link: "https://github.com/reasonml-community/reason-react-hacker-news", - }, - { - name: "TodoMVC", - image: "img/examples/todomvc.png", - link: "https://github.com/reasonml-community/reason-react-example/tree/master/src/todomvc", - }, + { + name: "Hacker News", + image: "img/examples/hn.png", + link: "https://github.com/reasonml-community/reason-react-hacker-news", + }, + { + name: "TodoMVC", + image: "img/examples/todomvc.png", + link: "https://github.com/reasonml-community/reason-react-example/tree/master/src/todomvc", + }, ]; -let reasonHighlightJs = require("reason-highlightjs"); +const reasonHighlightJs = require("reason-highlightjs"); const siteConfig = { - title: "ReasonReact", // title for your website - tagline: "All your ReactJS knowledge, codified.", - organizationName: "reasonml", - url: "https://reasonml.github.io/reason-react" /* your github url */, - editUrl: "https://github.com/reasonml/reason-react/tree/main/docs/", - translationRecruitingLink: "https://crowdin.com/project/reason-react", - baseUrl: "/reason-react/" /* base url for your project */, - projectName: "reason-react", - headerLinks: [ - { doc: "installation", label: "Docs" }, - { doc: "playground", label: "Try" }, - { doc: "simple", label: "Examples" }, - { doc: "community", label: "Community" }, - { blog: true, label: "Blog" }, - { languages: true }, - { search: true }, - { href: "https://github.com/reasonml/reason-react", label: "GitHub" }, - ], - users, - examples, - onPageNav: "separate", - scripts: ["/reason-react/js/pjax-api.min.js"], - /* path to images for header/footer */ - headerIcon: "img/reason-react-white.svg", - footerIcon: "img/reason-react-white.svg", - favicon: "img/reason-react-favicon.png", - /* colors for website */ - colors: { - primaryColor: "#48a9dc", - // darkened 10% - secondaryColor: "#2F90C3", - }, - // no .html suffix needed - cleanUrl: true, - highlight: { - theme: "atom-one-light", - hljs: function (hljs) { - hljs.registerLanguage("reason", reasonHighlightJs); - }, - }, - algolia: { - apiKey: "55156da6520de795d3a2c2d23786f08e", - indexName: "react-reason", - algoliaOptions: { - facetFilters: ["lang:LANGUAGE"], - }, - }, + title: "ReasonReact", // title for your website + tagline: "All your ReactJS knowledge, codified.", + organizationName: "reasonml", + url: "https://reasonml.github.io/reason-react" /* your github url */, + editUrl: "https://github.com/reasonml/reason-react/tree/main/docs/", + translationRecruitingLink: "https://crowdin.com/project/reason-react", + baseUrl: "/reason-react/" /* base url for your project */, + projectName: "reason-react", + headerLinks: [ + { doc: "installation", label: "Docs" }, + { doc: "playground", label: "Try" }, + { doc: "simple", label: "Examples" }, + { doc: "community", label: "Community" }, + { blog: true, label: "Blog" }, + { languages: true }, + { search: true }, + { href: "https://github.com/reasonml/reason-react", label: "GitHub" }, + ], + users, + examples, + onPageNav: "separate", + scripts: ["/reason-react/js/pjax-api.min.js"], + /* path to images for header/footer */ + headerIcon: "img/reason-react-white.svg", + footerIcon: "img/reason-react-white.svg", + favicon: "img/reason-react-favicon.png", + /* colors for website */ + colors: { + primaryColor: "#48a9dc", + // darkened 10% + secondaryColor: "#2F90C3", + }, + // no .html suffix needed + cleanUrl: true, + highlight: { + theme: "atom-one-light", + hljs: function (hljs) { + hljs.registerLanguage("reason", reasonHighlightJs); + }, + }, + algolia: { + apiKey: "55156da6520de795d3a2c2d23786f08e", + indexName: "react-reason", + algoliaOptions: { + facetFilters: ["lang:LANGUAGE"], + }, + }, }; module.exports = siteConfig;