Skip to content

Commit 6254b89

Browse files
authored
Merge pull request #124 from xsnippet/redux-avoid
[React-hooks] Update components to use hooks just to try them
2 parents 4dc1060 + 935ed28 commit 6254b89

File tree

12 files changed

+332
-420
lines changed

12 files changed

+332
-420
lines changed

src/actions/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const fetchSyntaxes = dispatch => (
4747
.then(json => dispatch(setSyntaxes(json)))
4848
)
4949

50-
export const postSnippet = (snippet, onSuccess) => dispatch => (
50+
export const postSnippet = (snippet, onSuccess, onError = () => {}) => dispatch => (
5151
fetch(getApiUri('snippets'), {
5252
method: 'POST',
5353
headers: {
@@ -61,4 +61,5 @@ export const postSnippet = (snippet, onSuccess) => dispatch => (
6161
dispatch(setSnippet(json))
6262
onSuccess(json)
6363
})
64+
.catch(onError)
6465
)

src/components/ListBoxWithSearch.jsx

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,45 @@
1-
import React from 'react'
1+
import React, { Fragment, useState, useCallback } from 'react'
22

33
import ListBox from './ListBox'
4+
45
import { regExpEscape } from '../misc/reqExp'
56

6-
class ListBoxWithSearch extends React.PureComponent {
7-
constructor(props) {
8-
super(props)
9-
this.state = {
10-
searchQuery: null,
11-
}
12-
}
7+
const ListBoxWithSearch = ({ items, onClick }) => {
8+
const [ searchQuery, setSearchQuery ] = useState(null)
139

14-
onSearch = e => {
15-
this.setState({ searchQuery: e.target.value.trim() })
16-
}
10+
const onSearch = useCallback(e => {
11+
setSearchQuery(e.target.value.trim())
12+
})
1713

18-
render() {
19-
const { searchQuery } = this.state
20-
let { items } = this.props
21-
22-
// Normalize items arrays so each item is always an object.
23-
items = items.map((item) => {
24-
if (item !== Object(item)) {
25-
return { name: item, value: item }
26-
}
27-
return item
28-
})
29-
30-
// Filter out only those items that match search query. If no query is
31-
// set, do nothing and use the entire set.
32-
if (searchQuery) {
33-
const regExp = new RegExp(regExpEscape(searchQuery), 'gi')
34-
items = items.filter(item => item.name.match(regExp))
14+
// Normalize items arrays so each item is always an object.
15+
items = items.map((item) => {
16+
if (item !== Object(item)) {
17+
return { name: item, value: item }
3518
}
3619

37-
return (
38-
[
39-
<div className="new-snippet-lang-header" key="Syntax input">
40-
<input className="input" placeholder="Type to search..." onChange={this.onSearch} />
41-
</div>,
42-
<div className="new-snippet-lang-list-wrapper" key="Syntax list">
43-
<ListBox
44-
items={items}
45-
onClick={this.props.onClick}
46-
/>
47-
</div>,
48-
]
49-
)
20+
return item
21+
})
22+
23+
// Filter out only those items that match search query. If no query is
24+
// set, do nothing and use the entire set.
25+
if (searchQuery) {
26+
const regExp = new RegExp(regExpEscape(searchQuery), 'gi')
27+
items = items.filter(item => item.name.match(regExp))
5028
}
29+
30+
return (
31+
<Fragment>
32+
<div className="new-snippet-lang-header" key="Syntax input">
33+
<input className="input" placeholder="Type to search..." onChange={onSearch} />
34+
</div>
35+
<div className="new-snippet-lang-list-wrapper" key="Syntax list">
36+
<ListBox
37+
items={items}
38+
onClick={onClick}
39+
/>
40+
</div>
41+
</Fragment>
42+
)
5143
}
5244

5345
export default ListBoxWithSearch

src/components/NewSnippet.jsx

Lines changed: 93 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useEffect, useRef } from 'react'
22
import { connect } from 'react-redux'
33
import AceEditor from 'react-ace'
44
import { WithContext as Tags } from 'react-tag-input'
@@ -7,166 +7,133 @@ import 'brace/theme/textmate'
77

88
import Notification from './common/Notification'
99
import ListBoxWithSearch from './ListBoxWithSearch'
10+
1011
import { fetchSyntaxes, postSnippet } from '../actions'
1112

12-
import { validateSnippet } from '../entries/snippetValidation'
13-
import { getCurrentModeName, getModesByName } from '../misc/modes'
1413
import { onEditorLoad } from '../misc/editor'
15-
import { recalcLangHeaderHeight } from '../misc/dom'
14+
import { getCurrentModeName, getModesByName } from '../misc/modes'
1615

16+
import { validateSnippet } from '../entries/snippetValidation'
1717
import { delimeterKeys } from '../entries/keyboardKeys'
1818
import { defaultOptions } from '../entries/aceEditorOptions'
1919

20-
import '../styles/NewSnippet.styl'
20+
import useForm from '../hooks/useForm'
2121

22-
class NewSnippet extends React.Component {
23-
constructor(props) {
24-
super(props)
22+
import '../styles/NewSnippet.styl'
2523

26-
this.state = {
27-
content: '',
28-
title: '',
29-
tags: [],
30-
syntax: '',
31-
validationError: null,
32-
}
33-
}
24+
const NewSnippet = ({ dispatch, history, syntaxes }) => {
25+
const snippetHeader = useRef()
26+
const {
27+
values: { title = '', syntax = '', content = '', tags = [] },
28+
error,
29+
handleChange,
30+
handleSubmit,
31+
} = useForm(post, validate)
3432

35-
componentDidMount() {
36-
const { dispatch } = this.props
33+
useEffect(() => {
3734
dispatch(fetchSyntaxes)
38-
}
39-
40-
onTagAdded = tag => {
41-
if (tag && tag.text) {
42-
this.setState({ tags: [...this.state.tags, tag] }, () => {
43-
recalcLangHeaderHeight()
44-
})
45-
}
46-
}
47-
48-
onTagRemoved = i => {
49-
const { tags } = this.state
35+
}, [])
5036

51-
this.setState({ tags: tags.filter((tag, index) => index !== i) }, () => {
52-
recalcLangHeaderHeight()
53-
})
54-
}
37+
useEffect(() => {
38+
recalcLangHeaderHeight()
39+
}, [tags])
5540

56-
onTagBlur = tag => {
57-
this.onTagAdded({ id: tag, text: tag })
41+
function validate() {
42+
return validateSnippet({ content: content.trim() })
5843
}
5944

60-
onSyntaxClick = syntax => {
61-
this.setState({ syntax })
45+
function post() {
46+
dispatch(postSnippet({
47+
content, title, tags: tags.map(tag => tag.text), syntax,
48+
}, json => history.push(`/${json.id}`)))
6249
}
6350

64-
onInputChange = e => {
65-
const { name, value } = e.target
51+
const recalcLangHeaderHeight = () => {
52+
const height = snippetHeader.current.offsetHeight
6653

67-
this.setState({ [name]: value })
54+
document.getElementsByClassName('new-snippet-lang-header')[0]
55+
.setAttribute('style', `height:${height}px`)
6856
}
6957

70-
validate = () => {
71-
const { content } = this.state
58+
const onTagBlur = tag => onTagAdded({ id: tag, text: tag })
7259

73-
return validateSnippet({ content: content.trim() })
60+
const onTagAdded = tag => {
61+
if (tag && tag.text) {
62+
return { tags: [...tags, tag] }
63+
}
7464
}
7565

76-
post = e => {
77-
e.preventDefault()
78-
const { dispatch, history } = this.props
79-
const { error } = this.validate()
80-
81-
this.setState({ validationError: error })
82-
83-
if (!error) {
84-
const {
85-
content, title, tags, syntax,
86-
} = this.state
87-
88-
dispatch(postSnippet({
89-
content, title, tags: tags.map(tag => tag.text), syntax,
90-
}, json => history.push(`/${json.id}`)))
91-
}
66+
const onTagRemoved = i => {
67+
return { tags: tags.filter((tag, index) => index !== i) }
9268
}
9369

94-
getSyntaxes = () => {
70+
const handleSyntax = syntax => ({ syntax })
71+
const handleContent = content => ({ content })
72+
73+
const getSyntaxes = () => {
9574
const { modesByName } = getModesByName()
9675

97-
return this.props.syntaxes.map(item => ({
76+
return syntaxes.map(item => ({
9877
name: modesByName[item].caption,
9978
value: item,
10079
}))
10180
}
10281

103-
renderValidationError = () => {
104-
const { validationError } = this.state
105-
106-
return validationError && <Notification
107-
message="Content is required :("
108-
show={!!validationError}
109-
/>
110-
}
111-
112-
render() {
113-
const { syntax, content, title, tags } = this.state
114-
115-
return (
116-
<form
117-
className="new-snippet"
118-
key="New Snippet"
119-
onSubmit={this.post}
120-
role="presentation"
121-
>
122-
<div className="new-snippet-code-wrapper">
123-
<div className="new-snippet-code-header">
124-
<input
125-
className="input"
126-
placeholder="Title"
127-
name="title"
128-
type="text"
129-
value={title}
130-
onChange={this.onInputChange}
131-
/>
132-
<Tags
133-
placeholder="Tags"
134-
tags={tags}
135-
handleDelete={this.onTagRemoved}
136-
handleAddition={this.onTagAdded}
137-
handleInputBlur={this.onTagBlur}
138-
delimiters={delimeterKeys}
139-
/>
140-
</div>
141-
<div className="new-snippet-code">
142-
<AceEditor
143-
mode={getCurrentModeName(syntax)}
144-
width="100%"
145-
height="100%"
146-
focus
147-
theme="textmate"
148-
onLoad={onEditorLoad}
149-
setOptions={defaultOptions}
150-
editorProps={{ $blockScrolling: Infinity }}
151-
value={content}
152-
onChange={(content) => { this.setState({ content }) }}
153-
/>
154-
155-
<div className="new-snippet-code-bottom-bar">
156-
{this.renderValidationError()}
157-
<input type="submit" value="POST SNIPPET" />
158-
</div>
159-
</div>
82+
const renderValidationError = () => (error && <Notification message={error} />)
83+
84+
return (
85+
<form
86+
className="new-snippet"
87+
key="New Snippet"
88+
onSubmit={handleSubmit}
89+
role="presentation"
90+
>
91+
<div className="new-snippet-code-wrapper">
92+
<div className="new-snippet-code-header" ref={snippetHeader}>
93+
<input
94+
className="input"
95+
placeholder="Title"
96+
name="title"
97+
type="text"
98+
value={title}
99+
onChange={handleChange}
100+
/>
101+
<Tags
102+
placeholder="Tags"
103+
tags={tags}
104+
handleDelete={(value) => handleChange(value, onTagRemoved)}
105+
handleAddition={(value) => handleChange(value, onTagAdded)}
106+
handleInputBlur={(value) => handleChange(value, onTagBlur)}
107+
delimiters={delimeterKeys}
108+
/>
160109
</div>
161-
<div className="new-snippet-lang-wrapper">
162-
<ListBoxWithSearch
163-
items={this.getSyntaxes()}
164-
onClick={this.onSyntaxClick}
110+
<div className="new-snippet-code">
111+
<AceEditor
112+
mode={getCurrentModeName(syntax)}
113+
width="100%"
114+
height="100%"
115+
focus
116+
theme="textmate"
117+
onLoad={onEditorLoad}
118+
setOptions={defaultOptions}
119+
editorProps={{ $blockScrolling: Infinity }}
120+
value={content}
121+
onChange={(value) => handleChange(value, handleContent)}
165122
/>
123+
<div className="new-snippet-code-bottom-bar">
124+
{renderValidationError()}
125+
<input type="submit" value="POST SNIPPET" />
126+
</div>
166127
</div>
167-
</form>
168-
)
169-
}
128+
</div>
129+
<div className="new-snippet-lang-wrapper">
130+
<ListBoxWithSearch
131+
items={getSyntaxes()}
132+
onClick={(syntax) => handleChange(syntax, handleSyntax)}
133+
/>
134+
</div>
135+
</form>
136+
)
170137
}
171138

172139
export default connect(state => ({

src/components/RecentSnippetItem.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react'
22
import { Link } from 'react-router-dom'
33

4-
import { getCurrentModeCaption } from '../misc/modes'
54
import { downloadSnippet } from '../misc/download'
5+
import { getCurrentModeCaption } from '../misc/modes'
66
import { getSnippetTitle, formatDate } from '../misc/snippet'
77
import { getRawUrl } from '../misc/url'
88

0 commit comments

Comments
 (0)