- {updateGistStatus.pending && 'Saving gist...'}
- {updateGistStatus.succeeded && 'Saved!'}
- {deleteGistStatus.pending && 'Deleting gist...'}
+ {updateGist.status.pending && 'Saving gist...'}
+ {updateGist.status.succeeded && 'Saved!'}
+ {deleteGist.status.pending && 'Deleting gist...'}
-
- Description:
-
+
Description:
+ onChange={this.onDescriptionChange}
+ />
{_.map(files, (file, originalFilename) => {
@@ -73,12 +73,17 @@ class Gist extends Component {
type="text"
className="gist_fileNameInput"
value={file.filename}
- onChange={(event) => this.onFileNameChange(originalFilename, event)}/>
+ onChange={event =>
+ this.onFileNameChange(originalFilename, event)
+ }
+ />
);
})}
@@ -93,11 +98,10 @@ class Gist extends Component {
constructor(props) {
super(props);
- const gist = this.props.gist || {};
- const {
- description = '',
- files = []
- } = gist;
+ const { gistId, requests } = props;
+ const { readGist } = requests;
+ const gist = readGist.resources[gistId] || {};
+ const { description = '', files = [] } = gist;
this.state = {
description,
@@ -105,39 +109,29 @@ class Gist extends Component {
};
}
- componentDidMount() {
- this.readGist();
- }
-
- componentWillUnmount() {
- if (this.readGistXhr) {
- this.readGistXhr.abort();
- }
- }
-
+ // I need to update this logic to better handle caches
componentDidUpdate(prevProps) {
+ const { history, requests } = this.props;
+ const { readGist, updateGist, deleteGist } = requests;
const {
- deleteGistStatus, readGistStatus, updateGistStatus, resetUpdateGistStatus, history, gist
- } = this.props;
- const { gists, gistId } = prevProps;
+ readGist: prevReadGist,
+ updateGist: prevUpdateGist,
+ deleteGist: prevDeleteGist
+ } = prevProps.requests;
+ const { gistId } = prevProps;
- if (deleteGistStatus.succeeded) {
- const prevGistDeleteStatus = getStatus({ gists }, `gists.meta.${gistId}.deleteStatus`);
+ const gist = readGist.resources[gistId];
+ if (deleteGist.status.succeeded) {
// When we transition from pending to succeeded, then we know that the deletion was
// successful. When that happens, we redirect the user back to the homepage.
- if (prevGistDeleteStatus.pending) {
+ if (prevDeleteGist.status.pending) {
history.push('/');
}
}
- // These checks are a temporary way to handle fetching the "details" of a
- // gist. A Redux Resource plugin would handle this better with some metadata
- // on the resource. For more on plugins, refer to the documentation:
- // https://redux-resource.js.org/docs/guides/plugins.html
- if (readGistStatus.succeeded) {
- const prevGistReadStatus = getStatus({ gists }, `gists.meta.${gistId}.readStatus`);
- if (prevGistReadStatus.pending) {
+ if (readGist.status.succeeded) {
+ if (prevReadGist.status.pending) {
const newState = {
files: gist.files
};
@@ -150,40 +144,38 @@ class Gist extends Component {
}
}
+ // These checks are a temporary way to handle fetching the "details" of a
+ // gist. A Redux Resource plugin would handle this better with some metadata
+ // on the resource. For more on plugins, refer to the documentation:
+ // https://redux-resource.js.org/docs/guides/plugins.html
+
// If the request just succeeded, then we set a timer to reset the request back to a NULL
// state. That way, our success message disappears after a set amount of time.
- if (updateGistStatus.succeeded) {
- const prevGistUpdateStatus = getStatus({ gists }, `gists.meta.${gistId}.updateStatus`);
- if (prevGistUpdateStatus.pending) {
- this.resettingUpdate = setTimeout(() => resetUpdateGistStatus(gistId), 1500);
+ if (updateGist.status.succeeded) {
+ if (prevUpdateGist.status.pending) {
+ this.resettingUpdate = setTimeout(
+ () => updateGist.setFetchToIdle(),
+ 1500
+ );
}
}
}
- readGist = () => {
- const { readGist, gistId } = this.props;
-
- if (this.readGistXhr) {
- this.readGistXhr.abort();
- }
-
- this.readGistXhr = readGist(gistId);
- }
-
- deleteGist = () => {
- const { gistId, deleteGist } = this.props;
+ syncStateWithGist = () => {};
+ confirmDelete = (e, deleteGist) => {
+ e.preventDefault();
const confirmedDelete = window.confirm(
'Are you sure you wish to delete this gist? This cannot be undone.'
);
if (confirmedDelete) {
- deleteGist(gistId);
+ deleteGist();
}
- }
+ };
- saveGist = () => {
- const { gistId, updateGist } = this.props;
+ saveGist = (e, updateGist) => {
+ e.preventDefault();
const { description, files } = this.state;
// We may have a timer already set to "reset" the
@@ -193,17 +185,19 @@ class Gist extends Component {
// For more, see `componentDidUpdate`
clearTimeout(this.resettingUpdate);
- updateGist(gistId, {
- description,
- files
+ updateGist({
+ body: JSON.stringify({
+ description,
+ files
+ })
});
- }
+ };
- onDescriptionChange = (event) => {
+ onDescriptionChange = event => {
this.setState({
description: event.target.value
});
- }
+ };
onFileNameChange = (oldFilename, event) => {
const { files } = this.state;
@@ -214,7 +208,7 @@ class Gist extends Component {
this.setState({
files: clonedFiles
});
- }
+ };
onFileContentsChange = (oldFilename, event) => {
const { files } = this.state;
@@ -225,54 +219,29 @@ class Gist extends Component {
this.setState({
files: clonedFiles
});
- }
-}
-
-function mapStateToProps(state, props) {
- const { gists } = state;
- const { match } = props;
- const { gistId } = match.params;
-
- const gist = gists.resources[gistId];
-
- // The third argument here is `treatNullAsPending`. This means that requests with a
- // null status will be returned as pending, which is ideal for requests that occur
- // when a page first loads. For more, refer to the Tips section of the `getStatus`
- // documentation:
- // https://redux-resource.js.org/docs/api-reference/get-status.html#tips
- const readGistStatus = getStatus(state, `gists.meta.${gistId}.readStatus`, true);
-
- // We're using the HTTP Status Code plugin to determine if the error is a 404. Typically,
- // if you're using standard HTTP requests in your application, you'll want to include the
- // HTTP Status Codes plugin. You can see how this is set up by referring to the gists
- // reducer file. For more on the HTTP Status Codes plugin, see the docs at:
- // https://redux-resource.js.org/docs/extras/http-status-codes-plugin.html
- const gistNotFound = _.get(state, `gists.meta.${gistId}.readStatusCode`) === 404;
-
- // These requests are initiated by a user's action, so we do not pass `treatNullAsPending`
- // as `true`. Otherwise, the interface would always display a loading indicator to the user.
- // Not sure what I mean? Try it out, and you can see what happens.
- const deleteGistStatus = getStatus(state, `gists.meta.${gistId}.deleteStatus`);
- const updateGistStatus = getStatus(state, `gists.meta.${gistId}.updateStatus`);
-
- return {
- gists,
- gistId,
- gist,
- gistNotFound,
- readGistStatus,
- deleteGistStatus,
- updateGistStatus
};
}
-function mapDispatchToProps(dispatch) {
- return bindActionCreators({
- readGist,
- updateGist,
- deleteGist,
- resetUpdateGistStatus
- }, dispatch);
+// This component replaces the role of `connect()` from react-redux.
+// It's a separate component so that we get our data in the lifecycle
+// methods above
+export default function GistResources(routeParams) {
+ const { gistId } = routeParams.match.params;
+
+ return (
+
,
+
,
+
+ ]}>
+ {([readGist, updateGist, deleteGist]) => (
+
+ )}
+
+ );
}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Gist);
diff --git a/src/components/Gists.js b/src/components/Gists.js
index 17f8f0c..0f1828f 100644
--- a/src/components/Gists.js
+++ b/src/components/Gists.js
@@ -1,79 +1,37 @@
-import React, { Component } from 'react';
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
+import React from 'react';
import { Link } from 'react-router-dom';
-import { getResources, getStatus } from 'redux-resource';
import './Gists.css';
-import { readManyUsersGists } from '../state/gists/action-creators';
+import { ReadUsersGists } from '../request-components/Gists';
import login from '../personal-access-token';
const username = login.username;
-class Gists extends Component {
- render() {
- const { usersGists, usersGistsStatus } = this.props;
-
- return (
-
- {usersGistsStatus.pending && ('Loading gists...')}
- {usersGistsStatus.failed && (
-
- There was an error loading gists.
-
- )}
- {usersGistsStatus.succeeded && (
-
- {usersGists.map(gist => (
- -
- {username} /
-
- {Object.keys(gist.files)[0]}
-
-
- {!gist.public && '🔒'}
-
- ))}
-
- )}
-
- );
- }
-
- componentDidMount() {
- this.fetchUsersGists();
- }
-
- componentWillUnmount() {
- if (this.readManyUsersGistsXhr) {
- this.readManyUsersGistsXhr.abort();
- }
- }
-
- fetchUsersGists = () => {
- const { readManyUsersGists } = this.props;
-
- if (this.readManyUsersGistsXhr) {
- this.readManyUsersGistsXhr.abort();
- }
-
- this.readManyUsersGistsXhr = readManyUsersGists(username);
- }
+export default function Gists() {
+ return (
+
+ {({ status, lists, doFetch }) => (
+
+ {status.pending && 'Loading gists...'}
+ {status.failed && (
+
+ There was an error loading gists.{' '}
+
+
+ )}
+ {status.succeeded && (
+
+ {lists.usersGists.map(gist => (
+ -
+ {username} /
+ {Object.keys(gist.files)[0]}
+
+ {!gist.public && '🔒'}
+
+ ))}
+
+ )}
+
+ )}
+
+ );
}
-
-function mapStateToProps(state) {
- const usersGists = getResources(state.gists, 'usersGists');
- const usersGistsStatus = getStatus(state, 'gists.requests.getUsersGists.status', true);
-
- return {
- usersGists,
- usersGistsStatus
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return bindActionCreators({
- readManyUsersGists
- }, dispatch);
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Gists);
diff --git a/src/components/NewGist.js b/src/components/NewGist.js
new file mode 100644
index 0000000..c2c8d79
--- /dev/null
+++ b/src/components/NewGist.js
@@ -0,0 +1,80 @@
+import React, { Component } from 'react';
+import { Link } from 'react-router-dom';
+import { CreateGist } from '../request-components/Gists';
+
+export default class NewGist extends Component {
+ render() {
+ const { description } = this.state;
+
+ return (
+
+ {({ status, lists, doFetch }) => (
+
+ {status.succeeded && (
+
+ Your gist was successfully created.{' '}
+
+ Go to Gist details.
+
+
+ )}
+ {!status.succeeded && (
+
+
+
+ {status.pending && 'Creating gist...'}
+ {status.failed &&
+ 'An error occurred while creating the gist.'}
+
+
+
+ )}
+
+ )}
+
+ );
+ }
+
+ state = {
+ description: '',
+ files: {}
+ };
+
+ getCreateRequestBody = () => {
+ const { description } = this.state;
+
+ const gist = {
+ description,
+ public: true,
+ files: {
+ 'file1.txt': {
+ content: 'String file contents'
+ }
+ }
+ };
+
+ return {
+ body: JSON.stringify(gist)
+ };
+ };
+
+ onDescriptionChange = event => {
+ this.setState({
+ description: event.target.value
+ });
+ };
+}
diff --git a/src/index.js b/src/index.js
index 8b7dcd5..53dbeaa 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,21 +6,25 @@ import './index.css';
import App from './components/App';
import Gists from './components/Gists';
import Gist from './components/Gist';
-import CreateGist from './components/CreateGist';
+import NewGist from './components/NewGist';
import store from './state/store';
-ReactDOM.render((
+ReactDOM.render(
- (
-
-
-
-
-
-
-
- )}/>
+ (
+
+
+
+
+
+
+
+ )}
+ />
-
-), document.getElementById('root'));
+ ,
+ document.getElementById('root')
+);
diff --git a/src/request-components/Gists.js b/src/request-components/Gists.js
new file mode 100644
index 0000000..c764c0a
--- /dev/null
+++ b/src/request-components/Gists.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import { ResourceRequest, Fetch } from 'react-redux-resource';
+import headers from '../utils/headers';
+
+// The Redux Resource XHR library only exports bulk actions, so we use this
+// function to turn single-resource responses from the server into arrays.
+function singleResourceToArray(body) {
+ return [body];
+}
+
+export function ReadGist({ gistId, children }) {
+ const request = (
+
+ );
+
+ return (
+
[gist]}
+ resourceName="gists"
+ request={request}
+ children={children}
+ />
+ );
+}
+
+export function ReadUsersGists({ username, children }) {
+ const request = (
+
+ );
+
+ return (
+
+ );
+}
+
+export function CreateGist({ children }) {
+ const request = (
+
+ );
+
+ return (
+
+ );
+}
+
+export function UpdateGist({ gistId, children }) {
+ const request = (
+
+ );
+
+ return (
+
+ );
+}
+
+export function DeleteGist({ gistId, children }) {
+ const request = (
+
+ );
+
+ return (
+
+ );
+}
diff --git a/src/state/gists/reducer.js b/src/state/gists.js
similarity index 68%
rename from src/state/gists/reducer.js
rename to src/state/gists.js
index 1fb0c89..d1f6ecc 100644
--- a/src/state/gists/reducer.js
+++ b/src/state/gists.js
@@ -1,6 +1,9 @@
-import { resourceReducer } from 'redux-resource';
+import { resourceReducer } from 'react-redux-resource';
import { httpStatusCodes } from 'redux-resource-plugins';
+// This reducer manages our gists resource. I normally prefer a "ducks"-style
+// organization scheme, but with React Redux Resource, you don't need to do anything
+// other than define the reducer. Neat.
export default resourceReducer('gists', {
plugins: [
// This plugin gives us access to the HTTP Status Codes from our requests. The primary
@@ -8,8 +11,6 @@ export default resourceReducer('gists', {
// did the request fail because the user was unauthorized, or because the resource
// was not found? For more on the HTTP Status Codes plugin, see the documentation at:
// https://redux-resource.js.org/docs/extras/http-status-codes-plugin.html
- // If you're using, say, gRPC, then you would want to write a similar plugin that
- // handles the gRPC status codes.
httpStatusCodes
]
});
diff --git a/src/state/gists/action-creators.js b/src/state/gists/action-creators.js
deleted file mode 100644
index e79e40e..0000000
--- a/src/state/gists/action-creators.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { actionTypes } from 'redux-resource';
-import { crudRequest } from 'redux-resource-xhr';
-import headers from '../../utils/headers';
-
-// This file heavily leverages the Redux Resource XHR library. To learn
-// more about its API, refer to the documentation:
-// https://redux-resource.js.org/docs/extras/redux-resource-xhr.html
-
-// The Redux Resource XHR library only exports bulk actions, so we use this
-// function to turn single-resource responses from the server into arrays.
-function singleResourceToArray(body) {
- return [body];
-}
-
-export function createGist(gist) {
- const xhrOptions = {
- method: 'POST',
- url: 'https://api.github.com/gists',
- json: true,
- body: gist,
- headers
- };
-
- return dispatch => crudRequest('create', {
- actionDefaults: {
- resourceName: 'gists',
- request: 'createGist',
- list: 'createdGists',
- },
- transformData: singleResourceToArray,
- xhrOptions,
- dispatch
- });
-}
-
-export function resetCreateGistStatus() {
- return {
- type: actionTypes.CREATE_RESOURCES_NULL,
- resourceName: 'gists',
- request: 'createGist'
- };
-}
-
-export function readGist(gistId) {
- const xhrOptions = {
- method: 'GET',
- url: `https://api.github.com/gists/${gistId}`,
- json: true,
- headers
- };
-
- return dispatch => crudRequest('read', {
- actionDefaults: {
- resourceName: 'gists',
- resources: [gistId],
- },
- transformData: singleResourceToArray,
- xhrOptions,
- dispatch
- });
-}
-
-export function readManyUsersGists(username) {
- const xhrOptions = {
- method: 'GET',
- url: `https://api.github.com/users/${username}/gists`,
- json: true,
- headers
- };
-
- return dispatch => crudRequest('read', {
- actionDefaults: {
- resourceName: 'gists',
- request: 'getUsersGists',
- list: 'usersGists',
- mergeListIds: false,
- },
- xhrOptions,
- dispatch
- });
-}
-
-export function updateGist(gistId, gist) {
- const xhrOptions = {
- method: 'PATCH',
- url: `https://api.github.com/gists/${gistId}`,
- json: true,
- body: gist,
- headers
- };
-
- return dispatch => crudRequest('update', {
- actionDefaults: {
- resourceName: 'gists',
- resources: [gistId],
- },
- transformData: singleResourceToArray,
- xhrOptions,
- dispatch
- });
-}
-
-export function resetUpdateGistStatus(gistId) {
- return {
- type: actionTypes.UPDATE_RESOURCES_NULL,
- resourceName: 'gists',
- resources: [gistId]
- };
-}
-
-export function deleteGist(gistId) {
- const xhrOptions = {
- method: 'DELETE',
- url: `https://api.github.com/gists/${gistId}`,
- headers
- };
-
- return dispatch => crudRequest('delete', {
- actionDefaults: {
- resourceName: 'gists',
- resources: [gistId],
- },
- xhrOptions,
- dispatch
- });
-}
diff --git a/src/state/store.js b/src/state/store.js
index f5fc9a1..4e1ce30 100644
--- a/src/state/store.js
+++ b/src/state/store.js
@@ -1,16 +1,19 @@
-import { createStore, combineReducers, applyMiddleware } from 'redux';
+import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
-import gists from './gists/reducer';
+import gists from './gists';
// Redux Resource works best in conjunction with `combineReducers`.
// Although this project only has one resource, `gists`, using
// `combineReducers` allows you to add more resources as needed.
-// The docs for `combineReducers` can be found here:
+// For more, see the he documentation for `combineReducers`:
// http://redux.js.org/docs/api/combineReducers.html
const reducer = combineReducers({
gists
});
+// We set up Redux Devtools, just in case
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+
export default createStore(
reducer,
// We use the redux-thunk middleware to support making asynchronous
@@ -20,5 +23,5 @@ export default createStore(
//
// For more on redux-thunk, refer to the documentation:
// https://github.com/gaearon/redux-thunk
- applyMiddleware(thunk)
+ composeEnhancers(applyMiddleware(thunk))
);