Skip to content

Instantly share code, notes, and snippets.

@nfour
Last active September 7, 2024 13:09
Show Gist options
  • Save nfour/92b4a869de7b4b8a847e2bb1eb8c2d53 to your computer and use it in GitHub Desktop.
Save nfour/92b4a869de7b4b8a847e2bb1eb8c2d53 to your computer and use it in GitHub Desktop.
import { component, css } from '__react'
import { AsyncValue, Value } from './models/state'
export const MyRandomNumberGenerator = component<{
maximumGenerationAttempts: number
range: { from: number; to: number }
}>((props) => {
const { api } = useSomeRootState()
// `component.useState`, due to the name, gets picked up by react-refresh and operates correctly between hot-reloads
// Ideally this would be `component.state(() => ...)`
// Additionally, we are able to provide a `class` as the state initializer,
// which will be `makeAutoObservable`'d if there is no constructor present.
// With this we can achieve consistant syntax across all state definitions, whether local or global state
const state = component.useState(
() =>
class {
// `Value` is just a mobx boxed store: { value: T, set: (v: T) => void }
// We make `props` observable so we can react to changes within this class,
// without complicated useEffects
props = new Value(props)
generationCount = new Value(0)
get isOutOfAttempts() {
return (
this.generationCount.value >=
this.props.value.maximumGenerationAttempts
)
}
// AsyncValue is a mobx store which essentially wraps a async value ergonomically (similar to react-query?),
// providing .value, .error, .isPending, .query(), .set(), .progress, .clearQueue() etc.
// Types are also inferred nicely
myGeneratedNumber = new AsyncValue(
({ from, to }: { from: number; to: number }) =>
api
.fetchSomeNumber({ from, to })
.then((data) => data?.someNumber),
)
generateNumber = () => {
this.generationCount.set(this.generationCount.value + 1)
if (this.isOutOfAttempts) return
this.myGeneratedNumber.query({
from: this.props.value.range.from,
to: this.props.value.range.to,
})
}
},
)
// Whenever a new react prop object values change occurs,
// update the Value store with only those changed
component.onProps(props, state.props)
// effectively `useEffect(() => fn(), [])`
component.onMounted(() => {
state.generateNumber()
})
component.onUnmounted(() => {
// cleanup
})
// mobx autorun when there is only 1 function argument
component.onReaction(() => {
console.log('fetchin number', state.myGeneratedNumber.isPending)
})
// mobx reaction when there is 2 function arguments
component.onReaction(
() => state.myGeneratedNumber.value,
(newNumber) => {
console.log('cool a new number', newNumber)
},
)
return (
<>
{state.myGeneratedNumber.isPending && <div>Loading...</div>}
{state.myGeneratedNumber.error && (
<div>
Something broke {state.myGeneratedNumber.error?.message}
</div>
)}
<div>Your number: {state.myGeneratedNumber.value}</div>
<button
disabled={
state.myGeneratedNumber.isPending || state.isOutOfAttempts
}
onClick={state.generateNumber}
>
New number please!
</button>
<div>
Made {state.generationCount.value}/
<span
css={css`
${state.isOutOfAttempts &&
css`
color: red;
`}
`}
>
{props.maximumGenerationAttempts}
</span>{' '}
number generation requests so far.
</div>
{state.isOutOfAttempts && (
<div
css={css`
color: red;
`}
>
Reached maximum generation attempts
</div>
)}
</>
)
})
const useSomeRootState = () => {
return {
api: {
async fetchSomeNumber({
from,
to,
}: {
from: number
to: number
}) {
return {
// number in range:
someNumber: Math.floor(Math.random() * (to - from) + from),
}
},
},
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment