Skip to content

Commit

Permalink
♻️ Refactor local state to custom state management
Browse files Browse the repository at this point in the history
Using a custom state management system with hooks and context API. If this proves burdensome to maintain, I'm open to refactoring to alternative state management system. #26
  • Loading branch information
pete-murphy committed Jul 17, 2019
1 parent 6bf2845 commit 4d590da
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 56 deletions.
5 changes: 2 additions & 3 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import { search, results } from "store/reducer";

import "./App.scss";

const [SearchProvider, useSearch] = createStore(search);
const [ResultsProvider, useResults] = createStore(results);
export { useSearch, useResults };
export const [SearchProvider, useSearch] = createStore(search);
export const [ResultsProvider, useResults] = createStore(results);

function App() {
return (
Expand Down
10 changes: 6 additions & 4 deletions src/components/KeywordsInput.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { useState } from "react";
import React from "react";
import { setKeywords } from "store/actions";
import { useSearch } from "App";

const KeywordInput = () => {
const [{ keywords }, dispatch] = useSearch();

const handleChange = e => {
setKeywords(e.target.value);
dispatch(setKeywords(e.target.value));
};

const [keywords, setKeywords] = useState("");

return (
<label>
<span className="label-text">Keywords</span>
Expand Down
50 changes: 24 additions & 26 deletions src/components/LabelInput.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,51 @@
import React, { useState } from "react";
import uniq from "lodash/fp/uniq";
import { normalizeLabelString } from "lib";
import { TEST_IDS } from "./SearchForm";
import React from "react";

const SUGGESTED_LABELS = [
"good first issue",
"help wanted",
"first-timers-only"
];
import { TEST_IDS } from "./SearchForm";
import {
addSelectedLabel,
removeSelectedLabel,
setCurrentLabel
} from "store/actions";
import { useSearch } from "App";

const LabelInput = () => {
const [labelArray, setLabelArray] = useState(SUGGESTED_LABELS);
const [label, setLabel] = useState("");
const [
{ currentLabel, selectedLabels, suggestedLabels },
dispatch
] = useSearch();

const handleChange = e => {
setLabel(e.target.value);
dispatch(setCurrentLabel(e.target.value));
};
const handleRemoveLabel = label => _e => {
dispatch(removeSelectedLabel(label));
};

const handleSubmitLabel = e => {
const handleAddLabel = e => {
e.preventDefault();
const normalizedLabel = normalizeLabelString(label);
if (normalizedLabel !== "") {
setLabelArray(ls => uniq([...ls, normalizedLabel]));
setLabel("");
}
dispatch(addSelectedLabel(e.target.value));
};
return (
<label>
<span className="label-text">With labels</span>
<input
autoComplete="off"
list="labels"
value={label}
value={currentLabel}
placeholder="Add a label"
onChange={handleChange}
onKeyDown={e => e.keyCode === 13 && handleSubmitLabel(e)}
onBlur={handleSubmitLabel}
onKeyDown={e => e.keyCode === 13 && handleAddLabel(e)}
onBlur={handleAddLabel}
/>
<datalist id="labels">
{SUGGESTED_LABELS.filter(l => !labelArray.includes(l)).map(label => (
{suggestedLabels.map(label => (
<option key={label} value={label} />
))}
</datalist>
<ul data-testid={TEST_IDS.activeLabels}>
{labelArray.map(label => (
{selectedLabels.map(label => (
<li key={label}>
<button
onClick={_e => setLabelArray(ls => ls.filter(l => l !== label))}
>
<button onClick={handleRemoveLabel(label)}>
<span role="img" aria-label="X">
</span>
Expand Down
20 changes: 7 additions & 13 deletions src/components/LanguageInput.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import React, { useState } from "react";

export const LANGUAGES = [
"Any",
"Haskell",
"JavaScript",
"OCaml",
"PureScript"
];
import React from "react";
import { useSearch } from "App";
import { setSelectedLanguage } from "store/actions";

const LanguageInput = () => {
const [language, setLanguage] = useState(LANGUAGES[2]);
const [{ languages, selectedLanguage }, dispatch] = useSearch();

const handleChange = e => {
setLanguage(e.target.value);
dispatch(setSelectedLanguage(e.target.value));
};

return (
<label>
<span className="label-text">In language</span>
<select value={language} onChange={handleChange}>
{LANGUAGES.map(language => (
<select value={selectedLanguage} onChange={handleChange}>
{languages.map(language => (
<option key={language} value={language}>
{language}
</option>
Expand Down
23 changes: 15 additions & 8 deletions src/components/SearchForm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import React from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import SearchForm, { TEST_IDS } from "./SearchForm";
import { LANGUAGES } from "./LanguageInput";
import { SearchProvider } from "App";

afterEach(cleanup);

const renderWithProvider = () =>
render(
<SearchProvider>
<SearchForm />
</SearchProvider>
);

test("`LabelInput` adds a label to `labelArray`", () => {
const { getByLabelText, getByTestId } = render(<SearchForm />);
const { getByLabelText, getByTestId } = renderWithProvider();

const labelInput = getByLabelText(/label/i);
const activeLabels = getByTestId(TEST_IDS.activeLabels);
Expand All @@ -24,16 +31,16 @@ test("`LabelInput` adds a label to `labelArray`", () => {
});

test("`LanguageInput` selection works", () => {
const { getByLabelText } = render(<SearchForm />);
const { getByLabelText } = renderWithProvider();

const languageInput = getByLabelText(/language/i);

fireEvent.change(languageInput, { target: { value: LANGUAGES[1] } });
expect(languageInput).toHaveValue(LANGUAGES[1]);
fireEvent.change(languageInput, { target: { value: "Haskell" } });
expect(languageInput).toHaveValue("Haskell");
});

test("`KeywordsInput` selection works", () => {
const { getByLabelText } = render(<SearchForm />);
const { getByLabelText } = renderWithProvider();

const keywordsInput = getByLabelText(/keywords/i);

Expand All @@ -42,7 +49,7 @@ test("`KeywordsInput` selection works", () => {
});

test("`SearchForm` form submit works", () => {
const { getByLabelText } = render(<SearchForm />);
const { getByLabelText } = renderWithProvider();

const keywordsInput = getByLabelText(/keywords/i);
const languageInput = getByLabelText(/language/i);
Expand All @@ -51,7 +58,7 @@ test("`SearchForm` form submit works", () => {
fireEvent.change(keywordsInput, { target: { value: "Hello" } });
fireEvent.change(labelInput, { target: { value: "Foo" } });
fireEvent.keyDown(labelInput, { keyCode: 13 });
fireEvent.change(languageInput, { target: { value: LANGUAGES[1] } });
fireEvent.change(languageInput, { target: { value: "Any" } });

// @TODO: Implement the `handleSubmit` test
});
8 changes: 7 additions & 1 deletion src/store/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
ADD_SELECTED_LABEL,
REMOVE_SELECTED_LABEL,
SET_SELECTED_LANGUAGE,
SET_KEYWORDS
SET_KEYWORDS,
SET_CURRENT_LABEL
} from "store/constants";

export const addSelectedLabel = label => ({
Expand All @@ -24,3 +25,8 @@ export const setKeywords = keywords => ({
type: SET_KEYWORDS,
payload: keywords
});

export const setCurrentLabel = label => ({
type: SET_CURRENT_LABEL,
payload: label
});
1 change: 1 addition & 0 deletions src/store/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const ADD_SELECTED_LABEL = "ADD_SELECTED_LABEL";
export const REMOVE_SELECTED_LABEL = "REMOVE_SELECTED_LABEL";
export const SET_CURRENT_LABEL = "SET_CURRENT_LABEL";
export const SET_SELECTED_LANGUAGE = "SET_SELECTED_LANGUAGE";
export const SET_KEYWORDS = "SET_KEYWORDS";
export const FETCH_RESULTS_INIT = "FETCH_RESULTS_INIT";
Expand Down
11 changes: 10 additions & 1 deletion src/store/reducer/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
ADD_SELECTED_LABEL,
REMOVE_SELECTED_LABEL,
SET_SELECTED_LANGUAGE,
SET_KEYWORDS
SET_KEYWORDS,
SET_CURRENT_LABEL
} from "store/constants";

const DEFAULT_LABELS = ["good first issue", "help wanted", "first-timers-only"];
Expand All @@ -20,6 +21,7 @@ const LANGUAGES = [

const initialState = {
keywords: "",
currentLabel: "",
suggestedLabels: DEFAULT_LABELS,
selectedLabels: DEFAULT_LABELS,
languages: LANGUAGES,
Expand All @@ -46,11 +48,18 @@ const selectedLabels = (state = initialState.selectedLabels, action) => {
const reducer = (state = initialState, action) => {
switch (action.type) {
case ADD_SELECTED_LABEL:
return {
...state,
currentLabel: "",
selectedLabels: selectedLabels(state.selectedLabels, action)
};
case REMOVE_SELECTED_LABEL:
return {
...state,
selectedLabels: selectedLabels(state.selectedLabels, action)
};
case SET_CURRENT_LABEL:
return { ...state, currentLabel: action.payload };
case SET_SELECTED_LANGUAGE:
return { ...state, selectedLanguage: action.payload };
case SET_KEYWORDS:
Expand Down

0 comments on commit 4d590da

Please sign in to comment.