Welcome to the client-side documentation for our Backwoods app!
Listed alphabetically:
Alex Botello | Andrew Jarrett | John Coronel | Thuy Pham | Usman Javed | Victor Montoya |
---|---|---|---|---|---|
Github | Github | Github | Github | Github | Github |
At first we used Redux Form to manage form state, but because it stored everything on the store, it was causing the entire app to re-render whenever onChange was called.
Formik is becoming a standard library for managing React form state, and it doesn't use Redux to do it ✨
Formik uses React render props to render our form and passes in a bunch of great helpers as props.
Note: If you're not familiar with render props, Formik is a relatively gentle introduction to this pattern!
Render props are increasingly replacing higher-order components as the way to wrap components and add extra functionality.
All it means for us is that we pass a Formik component a prop called “render” whose value is a function that returns JSX. Don’t forget, render
isn’t some special name or a reserved word, it’s just the name of the prop that Formik looks for to determine what to, well, render.
Note: You've used render props with React Router before when you need extra control when rendering a particular route.
It’s worth learning how to use render props because awesome libraries like Formik inject a bunch of extra functionality and pass it to our component on props
.
import React from "react"
import { Formik } from "formik"
const handleSubmit = (values, actions) => {
// This is where we would call a Redux action:
console.log("Andrew’s new name is", values.name)
// We can use `actions` to do more advanced stuff:
actions.setSubmitting(false)
}
const renderFunction = props => (
<form onSubmit={props.handleSubmit}>
<input
name="name"
type="text"
onChange={props.handleChange}
onBlur={props.handleBlur}
value={props.values.name}
/>
<button type="submit">Submit</button>
</form>
)
const FormikExample = () => (
<div>
<h1>Let’s play “Change Andrew’s Name”!</h1>
<Formik
initialValues={{ name: "Andrew" }}
onSubmit={handleSubmit}
render={renderFunction}
/>
</div>
)
2 things to notice:
- The
onSubmit
prop expects a function that automatically receives 2 arguments from Formik:values : Object
— When we give an input itsname
prop, that input's value will be available on the values object at that key (for example,values.email
).actions : Object
— These are Formik actions, not Redux actions (although they are similar). You will use these far less than thevalues
argument, but it's there if you need it.
- The
render
prop expects a function that automatically receivesprops
from Formik, passing through any other props that you pass.
Formik components accept these props (with the props our app makes use of frequently in bold):
- component
- render: (props: FormikProps) => ReactNode
- children: func
- enableReinitialize?: boolean
- isInitialValid?: boolean
- initialValues?: Values
- onReset?: (values: Values, formikBag: FormikBag) => void
- onSubmit: (values: Values, formikBag: FormikBag) => void
- validate?: (values: Values) => FormikErrors | Promise
- validateOnBlur?: boolean
- validateOnChange?: boolean
- validationSchema?: Schema | (() => Schema)
Validation is easy with Formik, but beyond the scope of these docs.
Check out our app’s custom form validations, and consult the Formik validation docs for more info.
Let's talk state!
Note: Trips are stored together in an object, not an array.
Here is an example of how a trip is stored on state:
{
"8wer80-qwer08-er875-ef12d": {
id: "8wer80-qwer08-er875-ef12d",
name: "Trip 1",
// rest of Trip 1
},
"e2er79-df9r08-5r875-1fe2d": {
id: "e2er79-df9r08-5r875-1fe2d",
name: "Trip 2",
// rest of Trip 2
},
// rest of the trips ...
}
There are a couple reasons for this, most notably that object lookup happens in constant as opposed to linear time -- O(1)
instead of O(n)
.
2 things to make working with multiple trips easier:
- The
getTripsArray
function inselectors.js
takes the entire state and returns an array of trips - There is an array of trip IDs that you can loop through to iterate over all trips. For example:
// MUTATES STATE, DON'T ACTUALLY DO THIS:
state.trip.tripIds.forEach(tripid => {
state.trip.trips[tripid].isArchived = false
})
Have a component that needs access to the Redux store?
- Import
connect
from React Redux - Import any actions you want your component to call/dispatch (if applicable)
- Pick the parts of state you need with a
mapStateToProps
function - Pick the actions you need with a
mapDispatchToProps
function - Pass
mapStateToProps
andmapDispatchToProps
to theconnect
function, then to the next set of parens pass the component you're connecting - Export the connected component
Now the state/actions you need are now available on props!
For example, here is a simplified AppNav
component:
import React from "react"
import { connect } from "react-redux"
import { login, logout } from "../redux/actions/auth"
const AppNav = ({ logout, isLoggedIn }) => {
const isHomeOrAuthPath = isProtectedPath(pathname, protectedPaths)
return (
<div>
{isLoggedIn ? (
<button onClick={logout}>Log out</button>
) : (
<button onClick={login}>Log in</button>
)}
</div>
)
}
const mapStateToProps = state => ({
isLoggedIn: state.auth.isLoggedIn
})
const mapDispatchToProps = { login, logout }
export default connect(
mapStateToProps,
mapDispatchToProps
)(AppNav)
Note: Don't forget to pass
null
as the 1st argument toconnect
if you need to map an action, but don't need your component to subscribe to a slice of state.
The types.js
exposes as named exports all of our action types, e.g.:
export const LOGIN_SUCCESS = "LOGIN_SUCCESS"
Note that the constant name and string value map directly to each other; this is to give us code completion and ensure that we don't run into bugs that involve typos, which are notoriously difficult to debug.
Here's how state is mapped currently:
const createRootReducer = history =>
combineReducers({
trips: tripReducer,
auth: authReducer,
router: connectRouter(history)
})
Also note that if you need access to the router, it lives on state under the router
key. You should consider using React Router's withRoute
instead unless you're doing something advanced.
This is where our React components live.
Most components will live here as .js
files. However, there are a few exceptions:
- Forms
- Icons
- Pages
All forms live here! This is so we can standardize our form dependencies and form validations in 1 place.
Files:
formValidations
:: Our client-side form validations live here. To use validations, import the function(s) you need and hook them up to Formik (as of 1/3/19, Formik has yet to be added. See theformik
branch for more info).customInputs
:: Usually we want to display validation error messages alongside the offending input element. To prevent code duplication, import the input component you need in the form component you're writing.
All icons and svgs live here! These are housed separately to keep from cluttering up our components directory.
Pages are different from regular components in that they wrap up a number of smaller components as a larger component that is displayed when the user visits a particular route.
Note: Most pages use a
CustomRoute
component that lives in/src/utils
. This component handles protected routes (see the Routing section below). View component
Pages:
Dashboard
:: The Dashboard page is where our app lives. All routes here are mounted on/app
. To mount a new route, add an object todashboardRoutes
with the following format:
{
path: "/your-route", // Mounts to /app/your-route
name: "DOMDisplayName", // React browser extension uses this
component: YourComponent, // Don't forget to import your component!
exact: true // optional, defaults to false
}
-
LandingPage
:: This is mounted on our root route, and allows us to wrap the landing page with different styles/components than the rest of our app. Does little more than renderPages
(see next). -
Pages
:: Mounted on/pages
. To add a page that does not belong inside the dashboard (for example, a Pricing Page), add an object topagesRoutes
with the following format:
{
path: "/pricing", // Mounts to /pages/pricing
name: "Pricing", // React browser extension uses this
component: Pricing, // Don't forget to import your component!
exact: true // optional
},
Because our client-side code is a Node app until we build and ship it, we have access to process.env
.
For this reason we added a config directory to take advantage of this and export from config/index.js
a SERVER_URI
variable that our Redux actions read from and that changes depending on whether we're in a dev or prod env.
If you have any variables that might switch depending on context, put them here and make them available as named exports.
The Redux folder handles everything Redux!
Note: The "main" file for our store configuration lives in
client/store.js
.If you need to see where a particular slice of state lives, start there! (See What's this store.js file? for a synopsis).
The Redux folder contains 4 subdirectories:
actions
- The actions folder contains, as you guessed, our app's actions. It also contains ourtypes.js
file (see below for more info).reducers
- Our redux reducers. The file name for the actions that apply to which reducer should typically be named the same thing, e.g.actions/auth.js
andreducers/auth.js
.helpers
- Any helper function that helps us manage our actions or reducers should live heremiddleware
- Most middleware configuration currently lives inclient/store.js
. If configuring our middlewares gets any more complex, this folder is where we should move that configuration.
For more info: See also the Redux Section below
Styles and styled components live here!
If your styles get too big to live in the same file as your component, create a new file in the root of this folder following this naming convention:
src/styles/YourComponent.styles.js
maps to:
src/components/YourComponent.js
The fun part! This is where we declare our global styles, styled-components, and our theme.
The GlobalStyles
component can be added anywhere in our app as a sibling to the component in which those styles will take effect, e.g.:
<Root>
<GlobalStyles />
<App />
</Root>
GlobalStyles is a styled component that does not render its children, so in the example above, everything declared in GlobalStyles
applies to the App
component, which is its immediate sibling.
This is a good place to declare things like the font-size
that sets our rem
units, font-declarations, and any css resets/normalizations that should apply to the entire document.
This is where our cool, reusable components should live! For example, we have a custom Button
component. If you want to change how our button looks, do that here and it will be applied across our app.
Mixins don't make sense in a lot of contexts, but they're perfect for composing/reusing blocks of css! For example, we have a box-shadow mixin that looks like this:
export const boxShadowMixin = css`
box-shadow: 0 0 0.625rem 0 rgba(0, 0, 0, 0.1);
`
You can use that mixin anywhere inside a styled-component template string like this:
import styled from "styled-components"
import { boxShadowMixin } from "../theme/mixins"
const MyComponentStyles = styled.div`
// use the mixin like you would any variable in a template string:
${boxShadowMixin}
// renders:
// box-shadow: 0 0 0.625rem 0 rgba(0, 0, 0, 0.1);
width: 100px;
`
We also have a useful media
mixing that is defined like this:
const breakpoints = {
desktop: 1024,
tablet: 768,
phone: 576
}
export const media = Object.keys(breakpoints).reduce((acc, label) => {
acc[label] = (...args) => css`
@media (max-width: ${breakpoints[label] / 16}em) {
${css(...args)};
}
`
return acc
}, {})
You can use the media
mixin in any styled-component like so:
import styled from "styled-components"
import { media } from "../theme/mixins"
const MyComponentStyles = styled.div`
.someDiv {
${media.desktop`
display: flex;
color: orange;
`}
${media.tablet`display: inline-block;`}
${media.phone`display: none;`} */
}
`
This is where our theme lives! 🎉
A case might be made for renaming this file theme.js
.
This is where all of our constants are named. Themes in the styled-components
package are super powerful, and injected via context into our app.
The theme is available anywhere inside our app on props.theme
. You will almost always access the theme inside the styles for a styled-component.
To use the theme, you need to pass your styled-component a function that takes prop as an argument. This sounds more complicated than it is.
Here's how you will usually use our theme:
export const BannerStyles = styled.div`
.landing-page-banner {
background-color: ${props => props.theme.primaryDark};
}
`
To change the value of primaryDark
everywhere it's used in our app, simply change its value in variables.js
!
You can also access component props to render styles conditionally, depending on the value/existence of a particular prop. This is super cool, and is used in the banner animation (see animationRule
inside Presentational/Banner/styles.js
for an example that reads the animation's duration off props.seconds
).
This is where our frontend tests will live!
This is where we store utility functions that our app's frontend uses.
Used by our pages to protect routes on the frontend.
Most utility functions will live in this file as named exports. An example is the makeTaglineIterator
generator, which takes an array of banner taglines and yields the next tagline every time it is called:
export function* makeTaglineIterator(taglinesArray) {
let count = 0
while (count < Infinity) {
yield taglinesArray[count++ % taglinesArray.length]
}
}
These are where our Redux state selectors live as named exports (see Redux selectors section below).
Incomplete:
-
trip-tripcard-fix
branch:- Fix .btn:hover styling (styledComponents)
- Fix TripCard styling, remove Trip component
- Move boxshadow on card hover to mixins?
-
redux-state-tweaks
branch:- Fix bug where state.trip.trips changes based on archived/unarchived status (messes up things like tripIndex later)
- Rename trips reducer to just trip to avoid confusing state.trips.trips access
- Create state.trip.tripIds array that reads the keys on state.trips.trips to allow easier looping and other array conveniences
- Add Redux middleware depending on process.env.NODE_ENV (for example, remove redux-logger in prod)
- Implement errorHandler middleware for use in actions
- Fix bug where password hash stored on state when logged in (might need to set up routing?)
-
prop-types
branch:- [prop-types] Add PropTypes to all components
-
add-frontend-tests
branch:- Add Jest/Enzyme/Sinon and configure frontend tests
-
[add-formik]
branch:- Add validation to make sure start date comes before end date
- Let
initialValues
take care of placeholders; remove placeholder props
-
Unsorted:
- Refactor Nav scrollY into variable, or read from a prop (can then make this dynamic based on screen height?)
- Update Dashboard router to use Redirect.props.to.state (from) to redirect after login
- Add Pages:
- /login :: Login
- /signup :: Register
- Fix CustomRoute so it doesn't redirect when valid token exists
-
[fix-client-readme]
branch:- Write git push hook that runs doctoc automatically?
- Update environment variable
MONGO_URL_PROD
to URI throughout app
Complete:
-
[add-theme]
RemoveisSignedUp
from auth state