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

Dealing with form(s) state #422

Open
t3ddyK opened this issue Aug 13, 2019 · 2 comments
Open

Dealing with form(s) state #422

t3ddyK opened this issue Aug 13, 2019 · 2 comments

Comments

@t3ddyK
Copy link

t3ddyK commented Aug 13, 2019

I'm curios how people are handing form states during reducer state changes.

For example, I have a login form that takes 2 fields username and password.

On submission I dispatch an action

store.dispatch(LoginRequest(payload: .init(username: username, password: password)))

I have middleware that picks up this action and makes an async request to my auth provider.

On success or failure I dispatch the appropriate action

dispatch(LoginRequestSuccess(payload: .init(accessToken: "123", refreshToken: "456")))

or

dispatch(LoginRequestFailure(payload: .init(some_error)))

Currently my reducer looks something like this:

func authenticationReducer(_ action: Action, _ state: AuthenticationState?) -> AuthenticationState {
    var state = state ?? AuthenticationState()
    
    guard action is AuthenticationAction else { return state }
    
    switch action {
    case _ as LoginRequest:
        state.form = .dispatching
        
    case let action as LoginRequestSuccess:
        state.accessToken = action.payload.accessToken
        state.refreshToken = action.payload.refreshToken
        state.form = .success
        
    case let action as LoginRequestFailure:
        state.form = .fail
        state.error = action.payload.error

    default: break
    }
        return state
}

Based on state.form in my view controller I disable the form, show a loading indicator, show an error alert or reset the form and trigger a navigation.

This works really, however I have a few concerns:

  1. authenticationReducer should really only be concerned with auth state, not ui concerns such as form state

  2. I have numerous forms related to login / auth - reset password, recover username, trigger multi factor and so on, these additional states will all cause my reducer to grow and grow

  3. My actions become tied to my forms, should I create an action to refresh my tokens, I know need to pretty much duplicate LoginRequestSuccess to set new tokens in my store. Ideally I'd like my actions to be as generic and flexible as possible.

Coming from the React / Redux world I used Redux Form quite heavily, this let me keep track of form state in it's own separate reducer and provided callbacks such as onSubmitSuccess and onSubmitFail which allowed me to keep my ui state within the ui layer not mix my auth and form states.

@dani-mp
Copy link
Contributor

dani-mp commented Aug 13, 2019

I think your approach is quite ok. I can share some thoughts but note that I don't think you're doing anything wrong.

  1. You can have several reducers that respond to the same actions, so you can separate the place and logic where you handle some auth state and some UI state. At the end of the day, you're working in a frontend app, so almost everything is related to the UI, one way or another.
  2. Something I do to minimize saving a lot of "UI" state is having selectors that translate/derive my business logic into the state of my UI.
  3. This is programming in a nutshell, boilerplate vs. abstractions. If you see there are patterns in your app that arise often, consider creating some abstractions for it (factory functions to create actions with prefilled data, generic actions...).

For the last point, see that you can have more than one store in your app. You can have one locally for the form and its draft state, and then use the main one when it's finished.

@nodediggity
Copy link

nodediggity commented Aug 16, 2019

I've approached this by creating a formReducer and using regex to look for actions. I then keep my individual form state as a dictionary on the overall form state.

An example would be something like:

Actions

struct ResetPasswordRequest: Action, HasForm { var props: String }
struct ResetPasswordSuccess: Action, HasForm { }
struct ResetPasswordFailure: Action, HasError, HasForm { var payload: String }

FormState

enum AppForm: CaseIterable {
    case resetPass

    var key: String {
        switch self {
        case .resetPass: return "ResetPassword"
        }
    }
}

struct FormState: StateType {
    var forms: [AppForm: FormSubState] = [:]
}

struct FormSubState: StateType {
    var pending: Bool = false
    var success: Bool = false
    var error: String?
}

extension FormState: Hashable {}
extension FormSubState: Hashable {}

Reducer

func formReducer(_ action: Action, _ state: FormState?) -> FormState {
    var state = state ?? FormState()

    guard action is HasForm else { return state }

    let requestRgx = "^(.+)REQUEST$"
    let successRgx = "^(.+)SUCCESS$"
    let failureRgx = "^(.+)FAILURE$"

    let str = String(describing: type(of: action))

    func mapActionToForm(_ action: String) -> AppForm {
        guard let form = AppForm.allCases.first(where: { $0.key == action }) else { fatalError("Could not find form for action type \(action)") }
        return form
    }

    if requestRgx.test(str) {
        let matches = requestRgx.matches(str)
        state.forms[
            mapActionToForm(matches[1])
        ] = .init(pending: true, success: false, error: nil)
        return state
    }

    if successRgx.test(str) {
        let matches = successRgx.matches(str)
        state.forms[
            mapActionToForm(matches[1])
        ] = .init(pending: false, success: true, error: nil)
        return state
    }

    if failureRgx.test(str) {
        let error = (action as? HasError)?.payload
        let matches = failureRgx.matches(str)
        state.forms[
            mapActionToForm(matches[1])
        ] = .init(pending: false, success: false, error: error)
        return state
    }

    if action is ResetFormState {
        let state = FormState()
        return state
    }

    return state
}

This allows me to keep the reducer logic from growing and growing as I had more form states, instead I simply conform a form based action to HasForm and name my actions in a format that will be picked up by my regex.

I can now select my form state using an enum state?.forms[.resetPass] and react accordingly to any state changes.

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

No branches or pull requests

3 participants