-
Notifications
You must be signed in to change notification settings - Fork 0
Product Guide: Developer: Frontend
Our frontend is written using React and Redux architectures primarily, with the Material UI component library and various other supporting libraries--like our backend, we favored composition of the pieces we need over a monolithic solution. For dispatch, we made heavy use of the thunks, which is a middleware for Redux that allows for easier asynchronous state updates. Since frontend development for React's ecosystem is extremely well-documented, we tried as much as possible to adhere to convention so that new developers can rely on the established community documentation and support systems for React+Redux as well as the documenation we provide.
config configuration, provided by create-react-app
public static files that do not to be processed by Babel
scripts useful development utilities, called through yarn
src application source code
src/actions action creators, per http://redux.js.org/docs/basics/Actions.html#action-creators
src/components dumb components, per https://goo.gl/xDiV6C
src/containers smart containers, per https://goo.gl/xDiV6C
src/helpers useful helper functions
src/middleware redux middleware, per http://redux.js.org/docs/advanced/Middleware.html
src/reducers redux reducers, per http://redux.js.org/docs/basics/Reducers.html
In general, a container represents a "page" in our application, such as the listings view or the watched searches view. These are meant to be the primary holders of the state, and then pass that state as props through their child components.
Components are reusable UI elements, such as a card for displaying a listing's information. These are meant to be "dumb" -- making minimal direct use of the state, instead receiving the data they are meant to display via props passed by their parent containers.
In the Redux pattern, all of the application state is maintained in a single store. Any part of the app can call an action creator and dispatch the returned action. The actions contain data for the reducers to use to update the store. Actions are generally used for asynchronous operations (e.g. fetching listings from the backend) or updating parts of the application state that affect many different components.
In the Redux pattern, reducers receive dispatched actions and update the store according to the data in the action. Every element of state in the store has its own reducer that listens for dispatched actions and returns the new state of the store as a function of the action and the current state. In this way, all application data can be treated as immutable. The store only updates through the reducer processing dispatched actions, and everything that depends on the store is automatically updated after the store changes. This is the key benefit of the Redux pattern - data flows unidirectionally from the store down to the display components, and state is maintained functionally with no unexpected side effects.
Because every state update passes through the reducers, we are able to pass every state update through middleware to easily log state updates and handle errors. For example, we were able to write middleware that caught any instance of failure due to the user not being logged in and automatically requested a CAS login.
The entry point of our app is public/index.html, which is what is loaded when accessing the endpoint of the frontend dev server. The div with id="root" serves as the container into which the src/containers/Root.js is rendered.
The Root container does not directly handle any display elements; it applies the Material-UI theme, renders src/containers/App.js, and connects App to the Redux store.
The App container handles routing, rendering any of the following containers depending on the current URL route:
src/containers/Listing.js
src/containers/Listings.js
src/containers/Seek.js
src/containers/Seeks.js
src/containers/Watches.js
App also renders src/components/ComposeOverlay.js if the user has the Compose window open, renders Snackbars to give feedback to the user on the results of asynchronous actions, and renders src/components/FilterBar.js, which displays search filtering and sorting options when appropriate to the route.
The Listings, Seeks, and Watches containers display query results for listings, buy requests, and watched searches, respectively. The ListingsList, SeeksList, and WatchesList components display lists of ListingCard, SeekCard, and WatchCard, which are components for displaying the information of a single listing, buy request, or saved search. The Listing and Seek containers display instead a single ListingCard or SeekCard, and render permalink views.
It is useful to note that, while the user-facing name is Buy Requests, while developing the application we referred to buy requests as Seeks.
src/reducers/index.js contains a composeState reducer.
composeState contains the following fields:
-
show :: bool- whether the Compose overlay is currently visible -
isEdit :: bool- whether the Compose overlay is open for editing a post rather than composing a new one -
mode :: string- either "listings" or "seeks"; indicates the kind of post being composed -
listing :: object- the current listing being composed (undefinedif composing a buy request) -
seek :: object- the current seek being composed (undefinedif composing a listing)
The root view of the Compose workflow is in src/components/ComposeOverlay.js. Depending on composeState, the overlay renders either a ComposeForm (for composing listings) or a SeekComposeForm (for composing buy requests). We use redux-form for managing the state of the Compose form, allowing us to prepopulate the form with default values (or preexisting values in the case of editing a post) and send the user-inputted values to our server on submission.
The primary user flow of our application revolves around searching the main Listings and Seeks views, which is implemented with a cycle of actions and reducers to control the currentQuery in the Redux global store. User interactions with the search bar, filter bar, and card buttons dispatch loadListings or loadSeeks actions with a query object and optional reset parameter.
The currentQuery reducer patches every attribute of the dispatched query onto itself unless reset is set, in which case it will wipe the current query and replace it with the action's query. (Helpfully, this means that components do not need to be connected to the Redux store in order to update the currentQuery, allowing us to write much more modular "dumb" components.) For instance, when the Sort Order dropdown in the filter bar dispatches an action with a query like { order: 'creationDateDesc' }, the new query will be the same as the old query but with only the order changed. Conversely, when the user submits a new listing and is redirected to the "My Listings" page, the compose view dispatches an action with reset enabled and the query { isMine: true } to clear all of the other search parameters. In order to clear a field of the query, give undefined as the field's value: for instance, when the user clears the Created After field, the query { minCreateDate: undefined } is dispatched.
The modified query is then converted to a string by the query-string library for use as a parameter to a GET request for listings or buy requests from the backend. The backend returns an appropriate array of posts to display, and these overwrite the listings and seeks state.
Our application supports storing and retrieving the current query in the URL. Users can bookmark a link to the site and reopen that link to find the appropriate search results for their query. The parseQuery helper function parses the current URL route into a JavaScript object that is used to initialize the currentQuery in the store, parsing strings appropriately. Whenever the query is modified, the writeHistory helper method is used to update the route to reflect the current query.
To introduce a new query parameter on the frontend, one must:
- create a graphical element to allow the user to control this parameter and connect it
- add default values
parseQueryandstripQueryin the fileclient/src/helpers/query.jsto control when the parameter should be included in and retrieved from the URL - add backend support for parsing this parameter from the request URL
The currentQuery object has many of the same attributes as a Watched Search: this is no coincidence. Both the "Watch this Search" button on the Listings view and the "Notify Me" button on an individual buy request can be used to create a Watched Search, which is implemented by a postWatch action taking the currentQuery or seek as appropriate. Then, the "View Results" button just takes the watch object and issues a reset-ing LOAD_LISTINGS_REQUEST with that object as the query