Star Wars Autocomplete
Course
This project was built to teach the React State for Frontend Masters.
The Basics
If we wanted to fetch all of the characters, we could do something like this.
useEffect(() => {
console.log('Fetching');
fetch(`${endpoint}/characters`)
.then(response => response.json())
.then(response => {
console.log({ response });
setCharacters(Object.values(response.characters));
})
.catch(console.error);
});
A careful eye will see that this is actually triggering an infinite loop since we're calling this effect on every single render.
Adding a bracket will make this less bad.
useEffect(() => {
console.log('Fetching');
fetch(`${endpoint}/characters`)
.then(response => response.json())
.then(response => {
console.log({ response });
setCharacters(Object.values(response.characters));
})
.catch(console.error);
}, []);
This has its own catch since this will only run once when the component mounts and will never run again.
But we can burn that bridge later.
Okay, but we want this to run every time the search term changes.
Adding Loading and Error States
We'll need to keep track of that state.
const [loading, setLoading] = useState(true);
const [error, setError] = useState(error);
Updating the fetch effect.
useEffect(() => {
console.log('Fetching');
setLoading(true);
setError(null);
setCharacters([]);
fetch(`${endpoint}/characters`)
.then(response => response.json())
.then(response => {
setCharacters(Object.values(response.characters));
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, []);
Displaying It In The Component
<section className="sidebar">
{loading ? (
<p className="loading">Loading…</p>
) : (
<Characters characters={characters} />
)}
{error && <p className="error">{error.message}</p>}
</section>
Creating a Custom Hook
const useFetch = url => {
const [response, setResponse] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
console.log('Fetching');
setLoading(true);
setError(null);
setResponse(null);
fetch(url)
.then(response => response.json())
.then(response => {
setResponse(response);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return [response, loading, error];
};
Adding the Formatting in There
Let's break that out into a function.
const formatData = response => (response && response.characters) || [];
Then we can use it like this.
const [characters, loading, error] = useFetch(
endpoint + '/characters',
formatData,
);
We can add that to our useEffect
.
useEffect(() => {
console.log('Fetching');
setLoading(true);
setError(null);
setResponse(null);
fetch(url)
.then(response => response.json())
.then(response => {
setResponse(formatData(response));
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url, formatData]);
Using an Async Function
You can't pass an async function directly to useEffect
.
useEffect(() => {
console.log('Fetching');
setLoading(true);
setError(null);
setResponse(null);
const get = async () => {
try {
const response = await fetch(url);
const data = await response.json();
setResponse(formatData(data));
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
get();
}, [url, formatData]);
return [response, loading, error];
};
I don't like this, but you might.
Refactoring to a Reducer
const fetchReducer = (state, action) => {
if (action.type === 'FETCHING') {
return {
result: null,
loading: true,
error: null,
};
}
if (action.type === 'RESPONSE_COMPLETE') {
return {
result: action.payload.result,
loading: false,
error: null,
};
}
if (action.type === 'ERROR') {
return {
result: null,
loading: false,
error: action.payload.error,
};
}
return state;
};
Now, we can just dispatch actions.
const useFetch = (url, dependencies = [], formatResponse = () => {}) => {
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCHING' });
fetch(url)
.then(response => response.json())
.then(response => {
dispatch({
type: 'RESPONSE_COMPLETE',
payload: { result: formatResponse(response) },
});
})
.catch(error => {
dispatch({ type: 'ERROR', payload: { error } });
});
}, [url, formatResponse]);
const { result, loading, error } = state;
return [result, loading, error];
};
Dispensing Asynchronous Actions
Important: We're going to check out the asynchronous-actions
branch.
How could we right a simple thunk reducer?
const useThunkReducer = (reducer, initialState) => {
const [state, dispatch] = useReducer(reducer, initialState);
const enhancedDispatch = useCallback(
action => {
if (typeof action === 'function') {
console.log('It is a thunk');
action(dispatch);
} else {
dispatch(action);
}
},
[dispatch],
);
return [state, enhancedDispatch];
};
Now, we just use that reducer instead.
We can have a totally separate function for fetching the data that our state management doesn't know anything about.
const fetchCharacters = dispatch => {
dispatch({ type: 'FETCHING' });
fetch(endpoint + '/characters')
.then(response => response.json())
.then(response => {
dispatch({
type: 'RESPONSE_COMPLETE',
payload: {
characters: response.characters,
},
});
})
.catch(error => dispatch({ type: error, payload: { error } }));
};
Exercise: Implementing Character Search
There is a CharacterSearch
component. Can you you implement a feature where we update the list based on the search field?
import React from 'react';
import endpoint from './endpoint';
const SearchCharacters = React.memo(({ dispatch }) => {
const [query, setQuery] = React.useState('');
React.useEffect(() => {
dispatch({ type: 'FETCHING' });
fetch(endpoint + '/search/' + query)
.then(response => response.json())
.then(response => {
dispatch({
type: 'RESPONSE_COMPLETE',
payload: {
characters: response.characters,
},
});
})
.catch(error => dispatch({ type: error, payload: { error } }));
}, [query, dispatch]);
return (
<input
onChange={event => setQuery(event.target.value)}
placeholder="Search Here"
type="search"
value={query}
/>
);
});
export default SearchCharacters;
useEffect
and Dependencies
The Perils of We're going to need to important more things.
import { BrowserRouter as Router, Route } from 'react-router-dom';
import CharacterList from './CharacterList';
import CharacterView from './CharacterView';
Now, we'll add this little tidbit.
<section className="CharacterView">
<Route path="/characters/:id" component={CharacterView} />
</section>
In CharacterView
, we'll do the following refactoring:
const CharacterView = ({ match }) => {
const [character, setCharacter] = useState({});
useEffect(() => {
fetch(endpoint + '/characters/' + match.params.id)
.then(response => response.json())
.then(response => setCharacter(response.character))
.catch(console.error);
}, []);
// …
};
I have an ESLint plugin that solves most of this for us.
const CharacterView = ({ match }) => {
const [character, setCharacter] = useState({});
useEffect(() => {
fetch(endpoint + '/characters/' + match.params.id)
.then(response => response.json())
.then(response => setCharacter(response.character))
.catch(console.error);
}, []);
// …
};