Skip to content

Commit

Permalink
feat: improve usage of context
Browse files Browse the repository at this point in the history
  • Loading branch information
ghivert committed Aug 21, 2024
1 parent 1cb43c2 commit 4c83770
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 5 deletions.
21 changes: 21 additions & 0 deletions redraw/src/context.ffi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from "react"
import { jsx } from "./redraw.ffi.mjs"
import * as gleam from "./gleam.mjs"
import * as error from "./redraw/error.mjs"

const contexts = {}

export function contextProvider(context, value, children) {
return jsx(context.Provider, { value }, children)
}

export function createContext(name, defaultValue) {
if (contexts[name]) return new gleam.Error(new error.ExistingContext(name))
contexts[name] = React.createContext(defaultValue)
return new gleam.Ok(contexts[name])
}

export function getContext(name) {
if (!contexts[name]) return new gleam.Error(new error.UnknownContext(name))
return new gleam.Ok(contexts[name])
}
4 changes: 0 additions & 4 deletions redraw/src/redraw.ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,6 @@ export function coerce(value) {
return value
}

export function contextProvider(context, value, children) {
return jsx(context.Provider, { value }, children)
}

export function setCurrent(ref, value) {
ref.current = value
}
Expand Down
149 changes: 148 additions & 1 deletion redraw/src/redraw.gleam
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import gleam/function
import gleam/javascript/promise.{type Promise}
import gleam/option.{type Option}
import gleam/string
import redraw/error.{type Error}
import redraw/internals/coerce.{coerce}

// Component creation
Expand Down Expand Up @@ -313,18 +316,162 @@ pub fn use_context(context: Context(a)) -> a

/// Let you create a [context](https://redraw.dev/learn/passing-data-deeply-with-context) that components can provide or read.
/// [Documentation](https://redraw.dev/reference/redraw/createContext)
@deprecated("Use `redraw/create_context_` instead. `redraw/create_context` will be removed in 2.0.0. Unusable right now, due to how React handles Context.")
@external(javascript, "react", "createContext")
pub fn create_context(default_value default_value: Option(a)) -> Context(a)

/// Wrap your components into a context provider to specify the value of this context for all components inside.
/// [Documentation](https://redraw.dev/reference/redraw/createContext#provider)
@external(javascript, "./redraw.ffi.mjs", "contextProvider")
@external(javascript, "./context.ffi.mjs", "contextProvider")
pub fn provider(
context context: Context(a),
value value: a,
children children: List(Component),
) -> Component

/// Create a [context](https://redraw.dev/learn/passing-data-deeply-with-context)
/// that components can provide or read.
/// Each context is referenced by its name, a little bit like actors in OTP
/// (if you're familiar with Erlang). Because Gleam cannot execute code outside of
/// `main` function, creating a context should do some side-effect at startup.
///
/// In traditional React code, Context usage is usually written like this.
///
/// ```javascript
/// import * as react from 'react'
///
/// // Create your Context in a side-effectful way.
/// const MyContext = react.createContext(defaultValue)
///
/// // Create your own provider, wrapping your context.
/// export function MyProvider(props) {
/// return <MyContext.Provider>{props.children}</MyContext.Provider>
/// }
///
/// // Create your own hook, to simplify usage of your context.
/// export function useMyContext() {
/// return react.useContext(MyContext)
/// }
/// ```
///
/// To simplify and mimic that usage, Redraw wraps Context creation with some
/// caching, to emulate a similar behaviour.
///
/// ```gleam
/// import redraw
///
/// const context_name = "MyContextName"
///
/// pub fn my_provider(children) {
/// let assert Ok(context) = redraw.create_context_(context_name, default_value)
/// redraw.provider(context, value, children)
/// }
///
/// pub fn use_my_context() {
/// let assert Ok(context) = redraw.get_context(context_name)
/// redraw.use_context(context)
/// }
/// ```
///
/// Be careful, `create_context_` fails if the Context is already defined.
/// Choose a full qualified name, hard to overlap with inattention. If
/// you want to get a Context in an idempotent way, take a look at [`context()`](#context).
/// [Documentation](https://redraw.dev/reference/redraw/createContext)
@external(javascript, "./context.ffi.mjs", "createContext")
pub fn create_context_(
name: String,
default_value: a,
) -> Result(Context(a), Error)

/// Get a context. Because of FFI, `get_context` breaks the type-checker. It
/// should be considered as unsafe code. As a library author, never exposes
/// your context and expect users will call `get_context` themselves, but rather
/// exposes a `use_my_context()` function, handling the type-checking for the
/// user.
///
/// ```gleam
/// import redraw
///
/// pub type MyContext {
/// MyContext(value: Int)
/// }
///
/// /// `use_context` returns `Context(a)`, should it can be safely returned as
/// /// `Context(MyContext)`.
/// pub fn use_my_context() -> redraw.Context(MyContext) {
/// let context = case redraw.get_context("MyContextName") {
/// // Context has been found in the context cache, use it as desired.
/// Ok(context) -> context
/// // Context has not been found. It means the user did not initialised it.
/// Error(_) -> panic as "Unitialised context."
/// }
/// redraw.use_context(context)
/// }
/// ```
@external(javascript, "./context.ffi.mjs", "getContext")
pub fn get_context(name: String) -> Result(Context(a), Error)

/// `context` emulates classic Context usage in React. Instead of calling
/// `create_context_` and `get_context`, it's possible to simply call `context`,
/// which will get or create the context directly, and allows to write code as
/// if Context is globally available. `context` also tries to preserve
/// type-checking at most. `context.default_value` is lazily evaluated, meaning
/// no additional computations will ever be run.
///
/// ```gleam
/// import redraw
///
/// const context_name = "MyContextName"
///
/// pub type MyContext {
/// MyContext(count: Int, set_count: fn (Int) -> Nil)
/// }
///
/// fn default_value() {
/// let count = 0
/// les set_count = fn (_) { Nil }
/// MyContext(count:)
/// }
///
/// pub fn provider() {
/// use _, children <- redraw.component()
/// let context = redraw.context(context_name, default_value)
/// let #(count, set_count) = redraw.use_state(0)
/// redraw.provider(context, MyContext(count:, set_count:), children)
/// }
///
/// pub fn use_my_context() {
/// let context = redraw.context(context_name, default_value)
/// redraw.use_context(context)
/// }
/// ```
///
/// `context` should never fail, but it can be wrong if you use an already used
/// name.
pub fn context(name: String, default_value: fn() -> a) -> Context(a) {
case get_context(name) {
Ok(context) -> context
Error(get) ->
case create_context_(name, default_value()) {
Ok(context) -> context
Error(create) -> {
let get = " get_context: " <> string.inspect(get)
let create = " create_context_: " <> string.inspect(create)
let head = "[Redraw Internal Error] Unable to find or create context."
let body =
function.flip(string.join)(" ", [
"context should never panic.",
"Please, open an issue on https://github.com/ghivert/redraw,",
"and join the error details.\n",
])
let details = "Error details:"
let msg = string.join([head, body, details, get, create], "\n")
panic as msg
}
}
}
}

// API
//
/// Test helper to apply pending React updates before making assertions.
Expand Down
4 changes: 4 additions & 0 deletions redraw/src/redraw/error.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub type Error {
ExistingContext(name: String)
UnknownContext(name: String)
}

0 comments on commit 4c83770

Please sign in to comment.