Code Monkey home page Code Monkey logo

Comments (13)

Ben-G avatar Ben-G commented on May 29, 2024 13

There's no trivial answer to this. I've been meaning to scope out some ideas; but ⌚ is always an issue. I'll try to get this started with some general thoughts.

There are multiple considerations to think about:

  • Does your db layer do any sort of validation? (non-null constraints, foreign key constraints, etc.)
    • If it does, your state updates will have to wait for db updates to complete; or your db layer needs to be able to reset state, in case a validation failed
    • if it does not, things might become a little easier. But you still need to account for potential write errors to the db. How should state be affected if an update could not be written to the db?

With these considerations in mind I can think of a few general approaches:

  • Have a db layer subscribe to a store; filter out relevant state; persist it to the db (as suggested by @johnryan )
  • Have certain types of actions that trigger db writes and implement writing to the db as a middleware (as suggested by @AlexLittlejohn)
  • Treat the db similar to a webservice; as a failable outside resource. This would mean you would use ActionCreator or a similar concept to perform the db update before you update the state via reducers.

In my (so far fairly high level thoughts) I've been favoring the last approach. The advantage is, that there's a well defined way to defer state updates until the db writes are complete. If you treat the db as a webservice, you can use the same error handling paths for both.

A somewhat practical example might look like this:

func changeUsername(user: User, username: String) -> Store<State>.ActionCreator {
    return { state, store in

        do {
            try db.write(user)
        } catch {
            // log error
            return nil
        }

        store.dispatch(ChangeUsername(user, username))
    }
}

The big advantage of treating db writes like this, is that there is no magic. You control what is written to the db; you control the error handling and the resulting state updates - that avoids a lot of the difficulties that would come from trying to write a general solution for a db that traces the app state.

Notes on @johnryan's thoughts:

Alternately, the reducers could be in charge of saving incremental changes. This seems to have some benefits since the logic for a specific action is there already, although this feels wrong based on the design decisions of the architecture.

In deed, this should not happen in the reducers. Reducers should be side-effect free (very important for atomic state, dev tools, etc.). Therefore the suggesting of doing this prior to dispatching actions.

I hope this helps and I'm looking forward to discussing further thoughts :)

from reswift.

DivineDominion avatar DivineDominion commented on May 29, 2024 4

I'd love to implement a library where ReSwift events are stored in event streams. In my opinion, that's part of the beauty of this: to store the commands/events themselves, plus snapshots every N events for a particular object to keep it simple. Event sourcing always intrigued me personally, but I understand that this is not exactly what you'd expect to write in a Mac/iOS app. Then again, how is a web service different in nature?

This opens up the possibility to separate the read model from a write model more easily, or go as far as implement CQRS. This is not desirable in lots of cases, though :)

Both event sourcing and CQRS are natural extensions to an event-based library; but do they pay off?

The event store could be a temporary scratchpad only, like semi-persistent undo/redo actions which the DB reduces into your regular state every now and then.

from reswift.

ambientlight avatar ambientlight commented on May 29, 2024 1

In my (so far fairly high level thoughts) I've been favoring the last approach. The advantage is, that there's a well defined way to defer state updates until the db writes are complete. If you treat the db as a webservice, you can use the same error handling paths for both.

@Ben-G: Does your preferences generalizes to all side effects #64?
Let's say we have an example about more real use case like:

  1. Login
  2. Persist a session token.
  3. Fetch user profile details.
  4. Persist user profile entity.
  5. Fetch categories
  6. Persist categories

We basically have AsyncActionCreators chaining here, right? So if we would prefer to avoid it we will have to use things like Promises / RxSwift which will allow us nicely chaining different asynch operations, where we will have action dispatched in each single completion block?
So we would use some other library for resolving the callback hell?

The big advantage of treating db writes like this, is that there is no magic. You control what is written to the db; you control the error handling and the resulting state updates.

What about the separation of domains you mentioned in your talk? Like we will basically group together all actionCreators for network calls and say group together all actionCreators for persistence?

I don't have that much experience with redux, so it kinda feels that middleware fits the purpose for side effects much better. If say, we use the middleware approach like https://github.com/redux-saga/redux-saga, there doesn't seem to be any magic involved too. The client sends simple actions which our middleware:

  1. boxes it into another more complex action and passes it through
  2. performs the side-effects that will also result in dispatched action.

Then we are having one middleware for each domain/subdomains of our application. Middleware can depend on another middleware but it seems to exist nicely under unidirectional data flow. We have one middleware for network requests, one middleware for DB persistence that relies on previous. In case we disable one of these middleware, the actions will just propagate to reducer in its original form.

This also allows a great degree of flexibility where we can just swap one middleware with another one, something that likely will be more tedious when we have logic encapsulated action creators.

What do you think about it? Basically we are having middleware forming unidirectional side-effects workflows.

from reswift.

dani-mp avatar dani-mp commented on May 29, 2024 1

I would use a middleware for this that gets the current state, lets the action pass through, gets the state again, makes a diff of the changes and applies them to a db.

from reswift.

AlexLittlejohn avatar AlexLittlejohn commented on May 29, 2024

In my app I used a middleware for this which seems like a good choice.

Edit: wording

from reswift.

johnryan avatar johnryan commented on May 29, 2024

After giving this some thought I like the idea proposed by @Ben-G of using a db like a web service in the Action Creator.

This also helps if your UI relies on some sort of local ID. For instance with a note app you might want to instantly save a note to the app's state regardless of what the POST response will be. In this case you need some unique identifier (I typically just use a local db's row id) to reference the specific note in question even if just to provide a way to subsequently remove that record if the POST fails.

This approach allows you to get that identifier early enough in the lifecycle of the event so you can reference that identifier later in the lifecycle.

from reswift.

DivineDominion avatar DivineDominion commented on May 29, 2024

I always found DDD Repositories to be great for this: they publish a collection-like interface and can generate new (local) IDs based on internal requirements. I used UUID Strings for a few apps and liked that.

So the view tells its event handler the user wants to add a note; the event handler sets up a local note object with the ID and displays it immediately while firing off a request to persist the entity in the repository; if the repository is just an interface for a web service, failing to save can just as well result in emitting a "delete" event to remove the local Note and maybe show an error message.

from reswift.

Ben-G avatar Ben-G commented on May 29, 2024

I think we found an answer to this question? Feel free to reopen if issues with this approach come up.

from reswift.

DaniAkash avatar DaniAkash commented on May 29, 2024

@Ben-G For React and React-Native I can use redux-persist middleware to persist my entire redux store and rehydrate it when the app is re-opened. Is there any middlewares like this for ReSwift?

from reswift.

DivineDominion avatar DivineDominion commented on May 29, 2024

@DaniAkash there's no built-in solution for that. Have a look at https://github.com/ReSwift/ReSwift-Recorder -- that's the most prominent experimental out-of-the-box solution I know. It records the history, though, and you can make something similar using the payload of StandardAction (and make your custom Actions StandardActionConvertible): https://github.com/ReSwift/ReSwift/blob/master/ReSwift/CoreTypes/Action.swift#L26

Since there are no moving parts except the state itself, it should be very simple to convert your main state object to JSON or NSData.

from reswift.

oded-regev avatar oded-regev commented on May 29, 2024

I thought I'll share here my idea on how to implement it in the simplest way possible.

I created a singleton called ReduxPersist which persist the state to NSUserDefault with throttling of 1.0 seconds.

class ReduxPersist: NSObject, StoreSubscriber {
    static let shared = ReduxPersist()
    var timer: Timer?

    override init() {
        super.init()
        store.subscribe(self) { subcription in
            subcription.select { state in state }
        }
    }
    
    func newState(state: StateType?) {
        print("ReduxPersist --> new State")
        timer?.invalidate()
        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.persistStore), userInfo: nil, repeats: false);
    }
    
    @objc func persistStore() {
        store.state.persist()
    }
}

the method persist() is just an implementation of Codable for each StateType in my project.

from reswift.

UXDart avatar UXDart commented on May 29, 2024

it would be good to have a "builtin" solution for this, as we need it to work on updates for example. is there any plan to add persist / storage into the library? TIA

from reswift.

DivineDominion avatar DivineDominion commented on May 29, 2024

No, we don't plan to bake this in because it exceeds the scope of the state management library. If you e.g. use Realm and come up with a reusable set of components or extensions, please share the result so we can link to it!

from reswift.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.