Skip to content

Instantly share code, notes, and snippets.

@jaydenlin
Forked from rockchalkwushock/BookingForm.js
Created March 9, 2018 09:49
Show Gist options
  • Save jaydenlin/db8326c1fb65ff7fb3c8e6b37c5d32fd to your computer and use it in GitHub Desktop.
Save jaydenlin/db8326c1fb65ff7fb3c8e6b37c5d32fd to your computer and use it in GitHub Desktop.
Using redux-form with next.js
import axios from 'axios'
// Redux Constants
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS '
export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR'
// Redux Success Action
const fetchData = data => ({
type: FETCH_DATA_SUCCESS,
payload: data // Returns { message: 'Your email was sent!' }
})
// Redux Error Action
const fetchError = data => ({
type: FETCH_DATA_ERROR,
payload: data // Returns { error: 'There was a problem with your request, please try back later.' }
})
// FIXME: I don't want this exposed
// 1) Need to figure out how to properly get it injected at runtime.
// 2) How to get CircleCi to see it as well.
const url =
process.env.NODE_ENV === 'production'
? 'https://productionURL.now.sh'
: 'http://localhost:4000'
// Function for querying the api.
const mailClient = async input => {
try {
// POST to my backend.
const { data } = await axios.post(url, input)
return data
} catch (e) {
throw e
}
}
// Redux Async-Action-Creator
export const getBookingInfo = values => async dispatch => {
// Await the backend Promise of success or error.
const data = await mailClient(values)
// If an error let redux handle the proper UI message to be rendered.
if (data.error) return dispatch(fetchError(data))
// Dispatch to the redux store.
return dispatch(fetchData(data))
}
import React, { Component } from 'react'
import { Field, reduxForm } from 'redux-form'
import PropType from 'prop-types'
import { messages } from '../../lib' // ignore pertains to `react-intl`
import validate from './validate'
import Input from './Input'
import TextArea from './TextArea'
class BookingForm extends Component {
static propTypes = {
intl: PropType.object.isRequired
}
state = {
error: false,
isFetched: false
}
submitForm = async values => {
// Receive `getBookingInfo` from `<FormContainer />`
// Receive `reset` from reduxForm HOC.
const { getBookingInfo, reset } = this.props
return getBookingInfo(values).then(({ payload }) => {
if (payload.message) {
// Set state for successful server response.
this.setState(state => ({
...state,
isFetched: true
}))
} else if (payload.error) {
// Set state for failed server response.
this.setState(state => ({
...state,
error: true,
isFetched: true
}))
}
// Clear the form after submit.
reset()
})
}
// Render the appropriate response to the user on server response.
renderResponse = () => {
const { error, isFetched } = this.state
if (isFetched && error) {
return <strong>{this.props.intl.formatMessage(messages.errorRes)}</strong>
} else if (!error && isFetched) {
return (
<strong>{this.props.intl.formatMessage(messages.successRes)}</strong>
)
}
}
render() {
// Receive `handleSubmit` & `valid` from reduxForm HOC
// `intl` is from `react-intl` pay no attention.
const { handleSubmit, intl, valid } = this.props
// Pass `handleSubmit()` our declared function for handling the form submit event.
return (
<form onSubmit={handleSubmit(this.submitForm)}>
<div>
<Field
component={Input}
label={intl.formatMessage(messages.firstName)}
name="firstName"
type="text"
/>
</div>
<div>
<Field
component={Input}
label={intl.formatMessage(messages.lastName)}
name="lastName"
type="text"
/>
</div>
<div>
<Field
component={Input}
label={intl.formatMessage(messages.email)}
name="email"
type="email"
/>
</div>
<div>
<Field
component={Input}
label={intl.formatMessage(messages.phone)}
name="phone"
type="tel"
/>
</div>
<div>
<div>
<Field
component={TextArea}
label={intl.formatMessage(messages.message)}
name="message"
/>
</div>
</div>
<div>
{this.renderResponse()}
<button disabled={!valid} type="submit">
{intl.formatMessage(messages.book)}
</button>
</div>
</form>
)
}
}
// Wrap your form in the reduxForm HOC and pass options object as argument.
export default reduxForm({
form: 'booking',
validate // form level validation
})(BookingForm)
import { connect } from 'react-redux'
import { BookingForm } from '../components'
import { getBookingInfo } from '../lib'
// Just like a normal configuration of `react-redux`
// No need to tell the child component any state so we pass `null`
// as the first argument and then our actionCreator.
export default connect(null, {
getBookingInfo
})(BookingForm)
// pages/index.js
import { WithData } from '../lib'
import { FormContainer } from './containers'
// `next` will render this as localhost:XXXX/ and you will see your form
// again ignore `intl`
const Index = ({ intl }) => (
<FormContainer intl={intl} />
)
export default WithData(Index)
import React from 'react'
// Custom component to pass to <Field />
const Input = ({ label, input, type, meta: { error, touched } }) => (
<div>
<label htmlFor={label} />
<div>
<input {...input} placeholder={label} type={type} />
{touched && (error && <span>{error}</span>)}
</div>
</div>
)
export default Input
import { combineReducers, compose, createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { reducer as formReducer } from 'redux-form'
let reduxStore = null
// Create Root Reducer.
const rootReducer = combineReducers({
form: formReducer
})
const isProd = process.env.NODE_ENV === 'production'
const createMiddleware = () => {
const universalMiddleware = applyMiddleware(thunk)
// Don't use devTools in 'production', also don't exectue until
// `next` is running client side code! No access to `window`!
if (process.browser && window.devToolsExtension && !isProd) {
return compose(universalMiddleware, window.devToolsExtension())
}
return universalMiddleware
}
const getStore = initialState => {
let store
// I not client side or no reduxStore yet
if (!process.browser || !reduxStore) {
const middleware = createMiddleware()
store = createStore(rootReducer, initialState, middleware)
// If still not client side
if (!process.browser) {
return store
}
reduxStore = store
}
return reduxStore
}
export default getStore
import { Component } from 'react'
import { Provider } from 'react-redux'
import PropTypes from 'prop-types'
import { loadGetInitialProps } from 'next/dist/lib/utils'
import 'isomorphic-fetch'
import getStore from '../redux'
const getInitialState = reduxStore => ({
...reduxStore.getState()
})
// WithData(App)
export default ComposedComponent =>
class WithData extends Component {
static propTypes = {
initialState: PropTypes.object.isRequired
}
// All of this is executed server-side just like React's componentWillUnmount, hence you don't
// have access to things like `window` or `document` inside this function.
static async getInitialProps(ctx) {
const subProps = await loadGetInitialProps(ComposedComponent, ctx)
// Populate the reduxStore server-side.
const reduxStore = getStore({})
const props = { ...subProps }
// Return the initialState of the application and other props.
return {
initialState: getInitialState(reduxStore),
...props
}
}
constructor(props) {
super(props)
// Use the initialState created server-side to populate
// the reduxStore in the browser.
const reduxStore = getStore(this.props.initialState)
this.reduxStore = reduxStore
}
render() {
// Use <Provider /> just like you normally would with `redux`.
return (
<Provider store={this.reduxStore}>
<ComposedComponent {...this.props} />
</Provider>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment