-
-
Save jaydenlin/db8326c1fb65ff7fb3c8e6b37c5d32fd to your computer and use it in GitHub Desktop.
Using redux-form with next.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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