0
votes

How can I access the field values in a parent component of a redux-form component?

I'm not sure if it's caused by typescript, but before I started using typescript, I was able to access the form values through mapStateToProps just like how I have it currently. I've been trying to figure out what was different to my previous implementation but the only difference would be the versions of the npm dependencies and the addition of typescript.

LoginPage.tsx

import LoginForm from 'components/Forms/LoginForm'
import Layout from 'components/Layout'
import { StatusCodes } from 'lib/enums/statusCodes'
import { storeAuthToken } from 'lib/helpers/auth'
import { NextPage } from 'next'
import Router from 'next/router'
import React from 'react'
import { connect, DispatchProp } from 'react-redux'
import { FormInstance } from 'redux-form'

interface IProps {
  login: FormInstance<IFormData, IFormProps>
}

interface IState {
  errorMessage?: string,
  processing: boolean
}

interface IRootState {
  form: IProps
}

export interface IFormData {
  username?: string,
  password?: string
}

export interface IFormProps {
  contactId?: string,
  errorMessage?: string,
  fieldValues: Partial<IFormData>,
  processing: boolean
}

class LoginPage extends React.Component<NextPage & DispatchProp & IProps, IState> {
  state = {
    errorMessage: undefined,
    processing: false
  }

  setErrorMessage = (message: string) => {
    this.setState({
      errorMessage: message,
      processing: false
    })
  }

  handleSubmit = async (values: IFormData) => {
    if (values && values.username && values.password) {
      this.setState({
        errorMessage: undefined,
        processing: true
      })
  
      try {
        const { dispatch } = this.props
  
        await storeAuthToken(dispatch, values.username, values.password)
        Router.push('/')
      } catch (error) {
        if (error === StatusCodes.BAD_REQUEST) {
          this.setErrorMessage("Sorry, you have entered incorrect details. Please try again.")
        } else {
          this.setErrorMessage("Sorry, there was an issue trying to log you in")
        }
      }
    }
  }

  render() {
    const { login } = this.props
    const { processing } = this.state

    return (
      <Layout title="Login">
        <div className="form-wrapper full">
          <LoginForm processing={processing} onSubmit={this.handleSubmit} fieldValues={login.values} />
        </div>
      </Layout>
    )
  }
}

const mapStateToProps = ({ form: { login } }: IRootState) => ({ login })

export default connect(mapStateToProps)(LoginPage)

LoginForm.tsx

import Link from 'next/link'
import React from 'react'
import { Field, InjectedFormProps, reduxForm } from 'redux-form'
import FormButton from 'components/Forms/FormButton'
import Input from 'components/Fields/Input'
import { validateRequired } from 'lib/helpers/validators'
import { IFormProps, IFormData } from 'pages/login'

class LoginForm extends React.Component<IFormProps & InjectedFormProps<IFormData, IFormProps>> {
  render() {
    const { contactId, errorMessage, fieldValues, handleSubmit, processing } = this.props

    return (
      <form id="login" onSubmit={handleSubmit} >
        <h1>Sign in</h1>
        <fieldset>
          <div className="fields">
            {
              !contactId
                ? <Field name="username" type="text" component={Input} label="Username" validate={validateRequired} />
                : <Field name="username" type="email" component={Input} label="Email" validate={validateRequired} />
            } 
          </div>
          <div className="fields">
            <Field name="password" type="password" component={Input} label="Password" validate={validateRequired} />
          </div>
        </fieldset>
        { errorMessage && <p className="error-message">{errorMessage}</p> }
        <div className="form-bottom">
          <Link href="/"/*{`/forgot-password${fields.email ? `?email=${encodeURIComponent(fields.email)}` : ''}`}*/>
            <a className="inline">Forgotten your password?</a>
          </Link>
          <FormButton loading={processing}>
            Login
          </FormButton>
        </div>
      </form>
    )
  }
}

export default reduxForm<{}, IFormProps>({ form: 'login' })(LoginForm)

Here is my redux store file incase if that is coded incorrectly

import { createWrapper, HYDRATE, MakeStore } from 'next-redux-wrapper'
import { AnyAction, applyMiddleware, combineReducers, createStore, Reducer } from 'redux'
import { reducer as formReducer } from 'redux-form'
import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk'
import authReducer, { AuthState } from './auth/reducer'
import contactReducer, { ContactState } from './contact/reducer'
import initialState from './initialState'

export interface State {
  auth: AuthState
  contact: ContactState
}

const bindMiddleware = (middleware: [ThunkMiddleware]) => {
  if (process.env.NODE_ENV !== 'production') {
    const { composeWithDevTools } = require('redux-devtools-extension')
    return composeWithDevTools(applyMiddleware(...middleware))
  }

  return applyMiddleware(...middleware)
}

const combinedReducer = combineReducers({
  auth: authReducer,
  contact: contactReducer,
  form: formReducer
})

const reducer: Reducer = (state: State, action: AnyAction) => {
  if (action.type === HYDRATE) {
    const nextState: Reducer = {
      ...state,
      ...action.payload
    }

    return nextState
  } else {
    return combinedReducer
  }
}

const makeStore: MakeStore<State> = () => createStore(reducer, initialState, bindMiddleware([thunkMiddleware]))

export const wrapper = createWrapper<State>(makeStore/*, { debug: true }*/)
1
Is there an error? What are you expecting to happen? What happens instead? - cbr
@cbr The error says TypeError: Cannot destructure property 'login' of 'undefined' or 'null'. The way I wanted it to work was so that it returns the form state in redux so I can fetch the form values in "real-time" so whenever a new character is typed, the props will be updated with the new values. - Hyokune
In your reducer, return combinedReducer should probably be return combinedReducer(state, action) - cbr
@cbr It gives me this error Argument of type 'State' is not assignable to parameter of type 'CombinedState<{ auth: AuthState; contact: ContactState; form: FormStateMap; }>'. when I pass the two parameters to return combinedReducer. - Hyokune
Well, what does the rest of the error message say? I'm guessing it says that the State interface is missing the key and type for form. - cbr

1 Answers

0
votes

It seems like I missed out a key in the IApplicationState interface and as mentioned by @cbr, the parameters state and action needed to be passed to combinedReducer even though it doesn't directly take any.

Additionally it didn't like when the nextState constant had the type Reducer, so I have changed that to CombinedState<State> as well

The changed code looks like this

import { createWrapper, HYDRATE, MakeStore } from 'next-redux-wrapper'
import { AnyAction, applyMiddleware, combineReducers, createStore, Reducer } from 'redux'
import { reducer as formReducer } from 'redux-form'
import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk'
import authReducer, { AuthState } from './auth/reducer'
import contactReducer, { ContactState } from './contact/reducer'
import initialState from './initialState'

export interface State {
  auth: AuthState
  contact: ContactState,
  form: FormStateMap
}

const bindMiddleware = (middleware: [ThunkMiddleware]) => {
  if (process.env.NODE_ENV !== 'production') {
    const { composeWithDevTools } = require('redux-devtools-extension')
    return composeWithDevTools(applyMiddleware(...middleware))
  }

  return applyMiddleware(...middleware)
}

const combinedReducer = combineReducers({
  auth: authReducer,
  contact: contactReducer,
  form: formReducer
})

const reducer: Reducer = (state: State, action: AnyAction) => {
  if (action.type === HYDRATE) {
    const nextState: CombinedState<State> = {
      ...state,
      ...action.payload
    }

    return nextState
  } else {
    return combinedReducer(state, action)
  }
}

const makeStore: MakeStore<State> = () => createStore(reducer, initialState, bindMiddleware([thunkMiddleware]))

export const wrapper = createWrapper<State>(makeStore)