Switched redux to unstated

This commit is contained in:
JubbeArt 2018-07-16 20:12:01 +02:00
parent 2f40c15c7f
commit 43d5a1979c
14 changed files with 127 additions and 221 deletions

View file

@ -29,10 +29,9 @@
"babel-polyfill": "^6.26.0",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"redux": "^4.0.0",
"snuownd": "git+https://github.com/JordanMilne/snuownd.git",
"unstated": "^2.1.1",
"whatwg-fetch": "^2.0.4"
},
"devDependencies": {

View file

@ -1,18 +1,17 @@
import 'babel-polyfill'
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { store } from './state'
import { Provider } from 'unstated'
import Header from './pages/common/Header'
import About from './pages/about'
import Subreddit from './pages/subreddit'
import Thread from './pages/thread'
import NotFound from './pages/notFound'
ReactDOM.render(
<Provider store={store}>
<Provider>
<BrowserRouter basename={__dirname}>
<React.Fragment>
<Header />
@ -23,6 +22,7 @@ ReactDOM.render(
<Route path='/r/:subreddit/comments/:threadID/:junk/:commentID' component={Thread} />
<Route path='/r/:subreddit/comments/:threadID' component={Thread} />
<Route path='/r/:subreddit' component={Subreddit} />
<Route component={NotFound} />
</Switch>
</div>
</React.Fragment>

View file

@ -1,18 +1,13 @@
import React from 'react'
import { connect } from 'react-redux'
import {connect} from '../../state'
const StatusBox = props => (
<div id='status'>
{props.text &&
<p id='status-text'>{props.text}</p>}
<p id='status-text'>{props.gloabl.state.statusText}</p>}
{props.image &&
<img id='status-image' src={props.image} alt='status' />}
<img id='status-image' src={props.gloabl.state.statusImage} alt='status' />}
</div>
)
const mapStateToProps = state => ({
text: state.status.text,
image: state.status.image
})
export default connect(mapStateToProps)(StatusBox)
export default connect(StatusBox)

View file

@ -0,0 +1,5 @@
import React from 'react'
export default () => (
<h2>404 - Not found</h2>
)

View file

@ -1,7 +1,7 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { getRemovedThreadIDs } from 'api/removeddit'
import { getThreads } from 'api/reddit'
import { getRemovedThreadIDs } from '../../api/removeddit'
import { getThreads } from '../../api/reddit'
import Post from '../common/Post'
const getSubredditForAPI = props => {
@ -21,19 +21,20 @@ export default class Subreddit extends React.Component {
subreddit: ''
}
}
componentDidMount () {
this.updateThreads(this.props)
this.getRemovedThreads(this.props)
}
componentWillReceiveProps (nextProps) {
const newSubreddit = getSubredditForAPI(nextProps)
if (this.state.subreddit !== newSubreddit) {
this.updateThreads(nextProps)
this.getRemovedThreads(nextProps)
}
}
updateThreads (props) {
getRemovedThreads (props) {
const subreddit = getSubredditForAPI(props)
getRemovedThreadIDs(subreddit)
.then(threadIDs => getThreads(threadIDs))

View file

@ -1,5 +1,5 @@
import React from 'react'
import { prettyScore, prettyDate, parse } from 'utils'
import { prettyScore, prettyDate, parse } from '../../utils'
const Comment = (props) => {
let commentStyle = 'comment '

View file

@ -2,11 +2,7 @@ import React from 'react'
import Comment from './Comment'
import CommentInfo from './CommentInfo'
import SortBy from './SortBy'
import { connect } from 'react-redux'
import {
SORT_TOP, SORT_BOTTOM, SORT_NEW, SORT_OLD,
SHOW_ALL, SHOW_REMOVED_DELETED, SHOW_REMOVED, SHOW_DELETED
} from 'state'
import {connect, sort, filter} from '../../state'
import {
topSort, bottomSort, newSort, oldSort,
showRemovedAndDeleted, showRemoved, showDeleted
@ -94,22 +90,23 @@ const filterCommentTree = (comments, filterFunction) => {
const commentSection = (props) => {
console.time('render comment section')
const commentTree = unflatten(props.comments, props.root, props.removed, props.deleted)
const {commentFilter, commentSort} = props.global.state
if (props.show === SHOW_REMOVED_DELETED) {
if (commentFilter === filter.removedDeleted) {
filterCommentTree(commentTree, showRemovedAndDeleted)
} else if (props.show === SHOW_REMOVED) {
} else if (commentFilter === filter.removed) {
filterCommentTree(commentTree, showRemoved)
} else if (props.show === SHOW_DELETED) {
} else if (commentFilter === filter.deleted) {
filterCommentTree(commentTree, showDeleted)
}
if (props.sort === SORT_TOP) {
if (commentSort === sort.top) {
sortCommentTree(commentTree, topSort)
} else if (props.sort === SORT_BOTTOM) {
} else if (commentSort === sort.bottom) {
sortCommentTree(commentTree, bottomSort)
} else if (props.sort === SORT_NEW) {
} else if (commentSort === sort.new) {
sortCommentTree(commentTree, newSort)
} else if (props.sort === SORT_OLD) {
} else if (commentSort === sort.old) {
sortCommentTree(commentTree, oldSort)
}
console.timeEnd('render comment section')
@ -132,9 +129,4 @@ const commentSection = (props) => {
)
}
const mapStateToProps = state => ({
sort: state.commentSection.sort,
show: state.commentSection.show
})
export default connect(mapStateToProps)(commentSection)
export default connect(commentSection)

View file

@ -1,45 +1,26 @@
import React from 'react'
import {
setCommentSort,
setCommentShow,
SORT_TOP, SORT_BOTTOM, SORT_NEW, SORT_OLD,
SHOW_ALL, SHOW_REMOVED_DELETED, SHOW_REMOVED, SHOW_DELETED
} from 'state'
import { connect } from 'react-redux'
import {connect, sort, filter} from '../../state'
const sortBy = props => (
<div id='comment-sort'>
sorted by:
<span className='space' />
<select onChange={e => props.setSort(e.target.value)} defaultValue={props.sort}>
<option value={SORT_TOP}>top</option>
<option value={SORT_BOTTOM}>bottom</option>
<option value={SORT_NEW}>new</option>
<option value={SORT_OLD}>old</option>
<select onChange={e => props.global.setCommentSort(e.target.value)} defaultValue={props.global.state.commentSort}>
<option value={sort.top}>top</option>
<option value={sort.bottom}>bottom</option>
<option value={sort.new}>new</option>
<option value={sort.old}>old</option>
</select>
<span className='space' />
show:
<span className='space' />
<select onChange={e => props.setShow(e.target.value)} defaultValue={props.show}>
<option value={SHOW_ALL}>All comments</option>
<option value={SHOW_REMOVED_DELETED}>Removed and deleted</option>
<option value={SHOW_REMOVED}>Removed</option>
<option value={SHOW_DELETED}>Deleted</option>
<select onChange={e => props.global.setCommentFilter(e.target.value)} defaultValue={props.global.state.commentFilter}>
<option value={filter.all}>All comments</option>
<option value={filter.removedDeleted}>Removed and deleted</option>
<option value={filter.removed}>Removed</option>
<option value={filter.deleted}>Deleted</option>
</select>
</div>
)
const mapStateToProps = state => ({
sort: state.commentSection.sort,
show: state.commentSection.show
})
const mapDispatchToProps = dispatch => ({
setSort: sortString => dispatch(setCommentSort(sortString)),
setShow: showString => dispatch(setCommentShow(showString))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(sortBy)
export default connect(sortBy)

View file

@ -1,11 +0,0 @@
import React from 'react'
export default (props) => (
<React.Fragment>
<div id='comment-info'>
removed comments: {props.removedComments}/${props.totalComments}
({ ((100 * props.removedComments) / props.totalComments).toFixed(1) }%)
</div>
<div id='comment-sort'>sorted by: top</div>
</React.Fragment>
)

View file

@ -1,49 +0,0 @@
import { get, put } from 'utils'
// Sort types
export const SORT_TOP = 'SORT_TOP'
export const SORT_BOTTOM = 'SORT_BOTTOM'
export const SORT_NEW = 'SORT_NEW'
export const SORT_OLD = 'SORT_OLD'
// Filter types
export const SHOW_ALL = 'SHOW_ALL'
export const SHOW_REMOVED_DELETED = 'SHOW_REMOVED_DELETED'
export const SHOW_REMOVED = 'SHOW_REMOVED'
export const SHOW_DELETED = 'SHOW_DELETED'
// Action types
export const COMMENT_SORT = 'COMMENT_SORT'
export const COMMENT_SHOW = 'COMMENT_SHOW'
// Action creators
export const setCommentSort = payload => ({ type: COMMENT_SORT, payload })
export const setCommentShow = payload => ({ type: COMMENT_SHOW, payload })
const sortKey = 'commentSort'
const filterKey = 'commentFilter'
// Init state
const initialStatusState = {
sort: get(sortKey, SORT_TOP),
show: get(filterKey, SHOW_REMOVED_DELETED)
}
export const commentSectionReducer = (state = initialStatusState, action) => {
switch (action.type) {
case COMMENT_SORT:
put(sortKey, action.payload)
return {
...state,
sort: action.payload
}
case COMMENT_SHOW:
put(filterKey, action.payload)
return {
...state,
show: action.payload
}
default:
return state
}
}

View file

@ -1,14 +1,71 @@
import { createStore, combineReducers, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import { statusReducer } from './status'
import { commentSectionReducer } from './commentSection'
import React from 'react'
import { Subscribe, Container } from 'unstated'
import { get, put } from 'utils'
export { setStatusLoading, setStatusSuccess, setStatusError } from './status'
export * from './commentSection'
// Sort types for comments
export const sort = {
top: 'SORT_TOP',
bottom: 'SORT_BOTTOM',
new: 'SORT_NEW',
old: 'SORT_OLD'
}
const reducer = combineReducers({
status: statusReducer,
commentSection: commentSectionReducer
})
// Filter types for comments
export const filter = {
all: 'SHOW_ALL',
removedDeleted: 'SHOW_REMOVED_DELETED',
removed: 'SHOW_REMOVED',
deleted: 'SHOW_DELETED'
}
export const store = createStore(reducer, applyMiddleware(logger))
// Keys for localStorage
const sortKey = 'commentSort'
const filterKey = 'commentFilter'
class GlobalState extends Container {
constructor () {
super()
this.state = {
commentSort: get(sortKey, sort.top),
commentFilter: get(filterKey, filter.removedDeleted),
statusText: '',
statusImage: undefined
}
}
setCommentSort (sortType) {
put(sortKey, sortType)
this.setState({commentSort: sortType})
}
setCommentFilter (filterType) {
put(filterKey, filterType)
this.setState({commentFilter: filterType})
}
setErrorText (text) {
this.setState({statusText: text, statusImage: '/images/error.png'})
}
setSuccessText (text) {
this.setState({statusText: text, statusImage: '/images/done.png'})
}
setLoadingText (text) {
this.setState({statusText: text, statusImage: '/images/loading.gif'})
}
clearStatusText () {
this.setState({statusText: '', statusImage: undefined})
}
}
// A redux-like connect function for Unstated
export const connect = Component => {
return props => (
<Subscribe to={[GlobalState]}>
{gloablState => <Component {...props} global={gloablState} />}
</Subscribe>
)
}

View file

@ -1,46 +0,0 @@
const images = {
loading: '/images/loading.gif',
error: '/images/error.png',
success: '/images/done.png'
}
// Action types
const STATUS_SET_LOADING = 'STATUS_SET_LOADING'
const STATUS_SET_SUCCESS = 'STATUS_SET_SUCCESS'
const STATUS_SET_ERROR = 'STATUS_SET_ERROR'
// Action creators
export const setStatusLoading = (payload = '') => ({ type: STATUS_SET_LOADING, payload })
export const setStatusSuccess = (payload = '') => ({ type: STATUS_SET_SUCCESS, payload })
export const setStatusError = (payload = '') => ({ type: STATUS_SET_ERROR, payload })
// Init state
const initialStatusState = {
text: null,
image: null
}
export const statusReducer = (state = initialStatusState, action) => {
switch (action.type) {
case STATUS_SET_SUCCESS:
return {
...state,
text: action.payload,
image: images.success
}
case STATUS_SET_LOADING:
return {
...state,
text: action.payload,
image: images.loading
}
case STATUS_SET_ERROR:
return {
...state,
text: action.payload,
image: images.error
}
default:
return state
}
}

View file

@ -23,9 +23,6 @@ export const chunk = (arr, size) => {
return chunks
}
// JSON parsing for fetch
export const json = x => x.json()
// Make multiple requests to the same url, with an array of data (usually comment IDs)
// This is needed since there is a limit on how long a url can be
export const fetchMultiple = (url, arr, header, size = 100) => {
@ -34,7 +31,7 @@ export const fetchMultiple = (url, arr, header, size = 100) => {
return Promise.all(subArrays.map(subArr => window.fetch(url + subArr.join(), header)))
}
export const jsonMultiple = responses => Promise.all(responses.map(json))
export const jsonMultiple = responses => Promise.all(responses.map(response => response.json()))
// Change bases
export const toBase36 = number => parseInt(number, 10).toString(36)
@ -85,9 +82,10 @@ export const prettyScore = score => {
}
// Retrieve, store and delete stuff in the local storage
export const get = (key, defaultValue) => (
window.localStorage.getItem(key) !== null ? JSON.parse(window.localStorage.getItem(key)) : defaultValue
)
export const get = (key, defaultValue) => {
const value = window.localStorage.getItem(key)
return value !== null ? JSON.parse(value) : defaultValue
}
export const put = (key, value) => window.localStorage.setItem(key, JSON.stringify(value))

View file

@ -1532,6 +1532,10 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-context@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.1.6.tgz#0f425931d907741127acc6e31acb4f9015dd9fdc"
cross-spawn@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
@ -2915,7 +2919,7 @@ interpret@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
@ -3352,10 +3356,6 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
lodash-es@^4.17.5:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
@ -4379,17 +4379,6 @@ react-dom@^16.4.1:
object-assign "^4.1.1"
prop-types "^15.6.0"
react-redux@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
dependencies:
hoist-non-react-statics "^2.5.0"
invariant "^2.0.0"
lodash "^4.17.5"
lodash-es "^4.17.5"
loose-envify "^1.1.0"
prop-types "^15.6.0"
react-router-dom@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6"
@ -4486,13 +4475,6 @@ redux-logger@^3.0.6:
dependencies:
deep-diff "^0.3.5"
redux@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
dependencies:
loose-envify "^1.1.0"
symbol-observable "^1.2.0"
regenerate@^1.2.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
@ -5204,10 +5186,6 @@ supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0:
dependencies:
has-flag "^3.0.0"
symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
table@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
@ -5419,6 +5397,12 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
unstated@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/unstated/-/unstated-2.1.1.tgz#36b124dfb2e7a12d39d0bb9c46dfb6e51276e3a2"
dependencies:
create-react-context "^0.1.5"
upath@^1.0.5:
version "1.1.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"