Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve usage of context #3

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}