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

Mutating store state directly #1032

Open
mkhoussid opened this issue Dec 15, 2023 · 3 comments
Open

Mutating store state directly #1032

mkhoussid opened this issue Dec 15, 2023 · 3 comments
Labels
question Further information is requested

Comments

@mkhoussid
Copy link

mkhoussid commented Dec 15, 2023

Suppose the following:

import { createStore, createEvent } from 'effector'

const $store = createStore([42])
const event = createEvent()

$store.on(event, (state) => {
  . . .
  console.log('equal', state === $store.getState()) // -> true

  state.splice(0, 1)

  return state // do I even have to return anything here, if it is a direct mutation to the array object?
  . . .
})

event()

Could this become a problem if I decide to mutate this state as I did in the above code snippet?

Right now, I am doing something like this:

$store.on(event, (_state) => {
  const state = [..._state]

  console.log('equal', state === $store.getState()) // -> false

  state.splice(0, 1)

  return state
})

I ask, because direct mutation of state in other state managers (and even in React itself) goes against best practices.

If I cannot/should not mutate state directly, then what are my options here? What if $store.length === 1e6 and I need to frequently do some operations on it?

Another use case.

// CURRENTLY:
const $chats = createStore([]);
const updateChatList = createEvent();

$chats.on(updateChatList, (state, { payload: { chat } }) => {
	const chatInStateIndex = state.findIndex(({ id }) => id === chat.id);

	if (chatInStateIndex === -1) {
		return [chat, ...state];
	}

	const chats = [...state];
	chats.splice(chatInStateIndex, 1);

	return [chat, ...chats];
});
// If I am allowed to mutate state directly:
const $chats = createStore([]);
const updateChatList = createEvent();

$chats.on(updateChatList, (state, { payload: { chat } }) => {
	const chatInStateIndex = state.findIndex(({ id }) => id === chat.id);

	if (chatInStateIndex === -1) {
		state.unshift(chat);
		return;
	}

	state.splice(chatInStateIndex, 1);
	state.unshift(chat);
	return;
});
@mkhoussid mkhoussid added the question Further information is requested label Dec 15, 2023
@yumauri
Copy link
Contributor

yumauri commented Dec 15, 2023

Yes, mutating store value definitely will become a problem.

In the documentation this is pointed out several times:

https://effector.dev/en/api/effector/store/

Store is an object that holds the state value. Store is getting updates when receives a value that is not equal (!==) to current one and to undefined

https://effector.dev/en/api/effector/store/#on-triggers-reducer

reducer: Function that receives state and params and returns a new state, should be pure

https://effector.dev/en/explanation/glossary/#reducer

Reducer calculates a new state given the previous state and an event’s payload. For stores, if reducer returns undefined or the same state (===), then there will be no update for a given store.

There are few options here, but let me ask, do you have any performance issues with it, or are you just asking out of curiosity? If this is the first case, it is better to prepare an example repo with an issue. Otherwise there is no real use of such "what if's". And better place for such questions is in our community chats, than in github issues.

@mkhoussid
Copy link
Author

mkhoussid commented Dec 15, 2023

I asked because I have a scrollable list of messages, where each message has a clickable "header" if it quotes another message.

You can click on the header and it will scroll to that quoted message.

In addition, my backend can return a message ID to an effect. I sample that effect's doneData with source: $listNode, pass it onto a different effect, which then proceeds to access the list node and scroll to that message by ID.

I can write out some sample code if I didn't explain it well. The idea is that I need access to the list node and its attributes such as scrollTo not just in any component, but also outside.

But I guess a workaround would be to have another state variable like,

$scrollToMessageId = createStore<number | null>(null)

Then I'd set its value with the message ID that comes from the backend, and somewhere in a useEffect I'd invoke the scroll method and then reset $scrollToMessageId.

@NazariiShvets
Copy link

If I cannot/should not mutate state directly, then what are my options here? 
What if $store.length === 1e6 and I need to frequently do some operations on it?

You could put your value into a "box" and recreate this box when you do some mutation, so instead of copying your big array all you have to do is create new box

This could look something like this:

const $hugeList = createStore({ list: [] })
  .on(getData.doneData, (_, dateFromBackend) => ({ list: dataFromBackend }))
  .on(addNewItemToList, (current, newItem) => {
     const  { list } = current;
     list.push(newItem);
     return { list }
  })

This solution has some tradeoffs tho. You can't create computed stores with direct reference to list, you always need to use this "box-store", so update propagation will not stop because of same reference.

Example:

// bad, because `$fromToday` wouldn't be updated when `addNewItemToList` is called, 
// because $list references same object thus stops update propagation
const $list = $hugeList.map(box => box.list);
const $fromToday = $list.map(list => list.filter(item => item.createdAt > startOfToday())

// good, because it depends on reference check for `$hugeList` "box" 
// so when `addNewItemToList` creates new reference `$fromToday` is updatedProperly
const $fromToday = $hugeList.map(box => box.list.filter(item => item.createdAt > startOfToday()))

P.S. Array.prototype.filter is O(n) and if you have huge n-s you probably should avoid this, but example is here for the sake of simplicity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants