pmndrs / valtio

🧙 Valtio makes proxy-state simple for React and Vanilla
https://valtio.dev
MIT License
9.16k stars 257 forks source link

Read only property #233

Closed rmcarias closed 3 years ago

rmcarias commented 3 years ago

How can I expose a proxy property as ready only and say allow only updates to happen through an action?

dai-shi commented 3 years ago

Good question. As a proxy object is a mutable variable. You can mutate anyways. I'd say it's out of the scope of the library implementation, but it's nice to have such recipes: https://github.com/pmndrs/valtio#recipes

One naive idea is something like this:

// countState.ts

// do not export the proxy object
const state = proxy({ count })

// getter (non reactive)
export const getCount = () => state.count

// hook (reactive)
export const useCount = () => useSnapshot(state).count

// action
export const increment = () => ++state.count
rmcarias commented 3 years ago

@dai-shi thank you for this recommendation. It makes sense since we are not using the flux pattern.
I have another question:

if I have a store that has something like:

{
  animals:[],
  setAnimals (animals) { 
   // assume it's a class
  this.animals = animals;
 }
}

// and in my react component I use it like so

import store from 'mystore';

function Component() { 
    useEffect(() => {
      store.setAnimals(['zebra','horse','cats','dogs']);
  },[]);

return <>
   // I want to display the animals. Why should I ready from Snapshot vs just directly accessing 'store.animals'?
 </>;
}

// I want to display the animals. Why should I ready from Snapshot vs just directly accessing 'store.animals'? // What is the difference between using the 'snapshot' and just accessing the prop directly?

dai-shi commented 3 years ago

If you directly access store.animals, it won't trigger re-render. It will be just like this.

const store = {
  animals: [],
}

const Component = () => {
  useEffect(() => {
    store.animals = ['zebra','horse','cats','dogs']
  }, [])
  return <>{JSON.stringify(store.animals)}</>
)
rmcarias commented 3 years ago

@dai-shi Thanks for the feedback. So you are saying that in the above scenario, the store.animals acts like a useRef? I guess I am confused a bit because your examples have this:

setInterval(() => {
  ++state.count
}, 1000)

which mutates things directly

but also this:

// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

which mutates from snapshot. Which one is the one to use?

dai-shi commented 3 years ago

which mutates from snapshot

It doesn't mutate snapshot. It reads from snapshot. Inside onClick, it mutates state. Could you elaborate your confusion? Thanks. It's important for us to know how people read the docs.

rmcarias commented 3 years ago

@dai-shi I think I got it. I had to read the entire example again. When you referred to state.text and where you mention it will re-render on state.count but I assume {snap.count} is being watch and if a change happens it will re-render? Meaning, can I rewrite the above as `function Counter() {

return (

{state.count} // 👈🏼

) }` and get the same as above?

Maybe it's just me, but an example of how useSnapshot and state translate "similarish" to React's ( I know it's not how it works but for those moving from this pattern to Proxy it would help) const [state, setState] = useState() ...The docs don't have enough working examples of the usage combinations. 🤷🏼‍♂️

dai-shi commented 3 years ago

@dai-shi I think I got it. I had to read the entire example again. When you referred to state.text and where you mention it will re-render on state.count but I assume {snap.count} is being watch and if a change happens it will re-render?

"{snap.count} is being watch" is correct,

Meaning, can I rewrite the above as `function Counter() {

return (

{state.count} // 👈🏼 <button onClick={() => ++state.count}>+1

) }` and get the same as above?

and thus this is different, because {state.count} isn't watch.

Maybe it's just me, but an example of how useSnapshot and state translate "similarish" to React's ( I know it's not how it works but for those moving from this pattern to Proxy it would help) const [state, setState] = useState() ...The docs don't have enough working examples of the usage combinations. 🤷🏼‍♂️

valtio proxy is self-aware object, and you don't need a function like setState update state. If you prefer setState style, I recommend use-immer instead.

Would you elaborate explaining your use case please? Then, I think I can try creating code snippet, and hopefully we can improve docs.

rmcarias commented 3 years ago

@dai-shi I will create some examples. I have another question within this realm. I have this in a proxy file

Module: someotherproxystore

const anotherImportedProxyStore = proxy({uri:''});
export default anotherImportedProxyStore;

Module B:

import anotherImportedProxyStore from './someotherproxystore';

// this method is wrapped in a proxy
 async loadItineraries(): Promise<string[]> { 
 /// code
const uri = anotherImportedProxyStore.uri; 👈🏼   //is this correct?
await somethingAsync();
return Promise.resolve([]);
}

My question is, how to you use other proxies from with other proxies correctly? If I just import another proxy object, and use it in a method of another module, is it automatically watched or updated (not sure if I have the correct verbiage here).

dai-shi commented 3 years ago

There's no magic behind functions/methods (unless we use this, which is not recommended for beginners.)

// this method is wrapped in a proxy

It doesn't matter if a method is in a proxy or not.

const loadItineraries = async () => { 
  const uri = anotherImportedProxyStore.uri; 👈🏼 // this works, but it gets a string at evaluation once (meaning, not reactive)
  await somethingAsync();
  return Promise.resolve([]);
};

is it automatically watched or updated

So, no, it's not. What you are looking for is derive().

dai-shi commented 3 years ago

Closing as answered. Please open new issue or new discussion for further discussion.