-
-
Save koenbok/ae7b94f9fefccc16a34589af344db789 to your computer and use it in GitHub Desktop.
import * as React from "react"; | |
/** | |
A hook to simply use state between components | |
Warning: this only works with function components (like any hook) | |
Usage: | |
// You can put this in an central file and import it too | |
const useStore = createStore({ count: 0 }) | |
// And this is how you use it from any component | |
export function Example() { | |
const [store, setStore] = useStore() | |
const updateCount = () => setStore({ count: store.count + 1 }) | |
return <div onClick={updateCount}>{store.count}</div> | |
} | |
*/ | |
export function createStore<T>(state: T) { | |
// Store the initial state, copy the object if it's an object | |
let storeState: T = typeof state === "object" ? { ...state } : state | |
// Keep a list of all the listener, in the form of React hook setters | |
const storeSetters = new Set<Function>() | |
// Create a set function that updates all the listeners / setters | |
const setStoreState = (state: Partial<T>) => { | |
// If the state is an object, make sure we copy it | |
storeState = | |
typeof state === "object" ? { ...storeState, ...state } : state | |
// Update all the listeners / setters with the new value | |
storeSetters.forEach((setter) => setter(storeState)) | |
} | |
// Create the actual hook based on everything above | |
function useStore(): [T, typeof setStoreState] { | |
// Create the hook we are going to use as a listener | |
const [state, setState] = React.useState(storeState) | |
// If we unmount the component using this hook, we need to remove the listener | |
React.useEffect(() => () => storeSetters.delete(setState), []) | |
// But right now, we need to add the listener | |
storeSetters.add(setState) | |
// Return the state and a function to update the central store | |
return [state, setStoreState] | |
} | |
return useStore | |
} |
Good point, thanks!
Fixed a small issue that would warn: Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
This works for overrides too:
import * as React from "react"
import { Override } from "framer"
import { createStore } from "./Store"
const useStore = createStore({ rotate: 0 })
export function SetRotate(): Override {
const [store, setStore] = useStore()
return {
onTap() {
setStore({ rotate: store.rotate + 90 })
},
}
}
export function DoRotate(): Override {
const [store, setStore] = useStore()
return {
animate: { rotate: store.rotate },
}
}
@koenbok In the Override example it should be import { createStore } from "./store"
(uncapitalized), right?
For stores with many properties like:
{
title: 'ABC',
count: 1,
url: 'abc.com'
}
Am I right that you basically need to copy the whole object (all the properties) and then just overwrite the ones you want to update?
For stores with many properties like:
{ title: 'ABC', count: 1, url: 'abc.com' }
Am I right that you basically need to copy the whole object (all the properties) and then just overwrite the ones you want to update?
Yes you would need to do that when you set it if you use an object. You can also use the existing value and add on your new properties like this...
setStore({...store, url: 'framer.com'})
There are probably a few other ways to do that too.
@koenbok - Does using the store re-run the overrides like the data object?
This is very useful. One question from a React newbie. How can I make one sibling component rerender when the store gets changed by another sibling component?
Just out of interests, why not just use context for this?
React context changes cause all components in the context tree to re-render, which can cause slowdowns.
This is one reason that Redux tried and abandoned using context.
@koenbok Could you expand on this example, especially on the bit of a "central file"?
@maxsteenbergen I think I can help!
What you want is a a useStore.tsx file like this:
import { createStore } from "./store"
export const useStore = createStore({
//your store variables
})
And a store.ts file like this:
import * as React from "react"
import { useState, useEffect } from "react"
export function createStore<T>(state: T) {
let storeState: T = Object.assign({}, state)
const storeSetters = new Set()
const setStoreState = (state: Partial<T>) => {
storeState = Object.assign({}, storeState, state)
storeSetters.forEach(setter => setter(storeState))
}
function useStore(): [T, typeof setStoreState] {
const [state, setState] = useState(storeState)
useEffect(() => () => storeSetters.delete(setState), [])
storeSetters.add(setState)
return [state, setStoreState]
}
return useStore
}
Then you can import both into a component or an override:
import { createStore } from "./store"
import { useStore } from "./useStore"
And initialize them as such: const [store, setStore] = useStore()
@jacopocolo Thanks! Massively helpful!
I'm trying to update specific properties of a larger object store, but the below isn't working:
const moveHandler = (point, card_number) => { setStore(prevStore => { const newStore = { ...prevStore } newStore.last_position[card_number] = point.x return newStore }) }
Am I unable to access the previous store object as done above? This is how I set state in React, using the previous state object as an argument to setState, spreading this previous state object into the new state object, modifying the property within the new state object I'd like to change, and then returning the new state object.
Never mind, I figured it out. Simply create the new object and then return in setStore.
const moveHandler = (point, card_number) => { const newStore = { ...store } newStore.last_position[card_number] = point.x setStore(newStore) }
If I use the store.tsx in Framer (2020.31), it gives an error with setter
(line 23)
(parameter) setter: unknown
This expression is not callable.
Type '{}' has no call signatures.(2349)
How can I fix this?
If I use the store.tsx in Framer (2020.31), it gives an error with
setter
(line 23)(parameter) setter: unknown This expression is not callable. Type '{}' has no call signatures.(2349)
How can I fix this?
Try changing the second line in the function to:
const storeSetters = new Set<(state: T) => void>()
For an easy copy/paste:
import * as React from "react"
import { useState, useEffect } from "react"
/*
A hook to simply use state between components
Usage:
// You can put this in an central file and import it too
const useStore = createStore({ count: 0 })
// And this is how you use it from any component
export function Example() {
const [store, setStore] = useStore()
const updateCount = () => setStore({ count: store.count + 1 })
return <div onClick={updateCount}>{store.count}</div>
}
*/
export function createStore<T>(state: T) {
let storeState: T = Object.assign({}, state)
const storeSetters = new Set<(state: T) => void>()
const setStoreState = (state: Partial<T>) => {
storeState = Object.assign({}, storeState, state)
storeSetters.forEach((setter) => setter(storeState))
}
function useStore(): [T, typeof setStoreState] {
const [state, setState] = useState(storeState)
useEffect(() => () => storeSetters.delete(setState), [])
storeSetters.add(setState)
return [state, setStoreState]
}
return useStore
}
Just tried this now, awesome work @koenbok. I'm using createStore with a navigation code component and an overridden Framer Page component to easily emulate the navigation in a mobile app with custom effects etc.
This is so useful, I'm wondering if you should promote it more or ship it as part of Framer 💯 😃
@koenbok What's the difference between this and the Data()
function already available in Framer?
Not Koen but: as far as I understand the Data()
function only works between overrides. This is (potentially) shared across all overrides and components in a project.
@jacopocolo How would the code look like if you use this to share data between two different components that both have their own file? I do not understand where you initialise the store and the value you want to share.
For instance, I have these components:
- Slider.tsx
- Dial.tsx
I want to share a numeric value between the slider and the dial, where do I define the store and where do I initialise this value?
Many thanks in advance!
@DvDriel first of all you'd want to set up your store and impot like this: https://gist.github.com/koenbok/ae7b94f9fefccc16a34589af344db789#gistcomment-3214773
Then in your components you'd add:
const [store, setStore] = useStore()
This allows you to read the store as a regular variable {store.var}
. Or to write a variable with the setStore hook setStore({varName: value})
.
The store can contain a single variable or a set of objects.
@koenbok Would this also work in Framer Web? I tried to insert the code but get an error:
It seems like TypeScript got a bit stricter with warnings, but it should totally work.
Great, got it working indeed. What would be the right way to get this working on a class?
EDIT: nvm, figured out it was preferably to convert old class to a function. got it working now.
not to be a pedant, but it looks like useMemo is an unneeded import.
Also, personally, this is very timely. Thanks for sharing 👍