I recently built a small film app to browse and save favorite movies, but I’m running into bugs with search results not updating correctly and favorites not saving between sessions. I’ve double-checked my API calls and local storage setup, but the issues keep happening. Can someone help me figure out what I might be doing wrong and suggest best practices for a stable film app?
Two separate issues here: search state and persistence.
- Search results not updating
Common causes:
• Stale state
If you use React:
-
Make sure the search term is part of state, not a plain variable.
-
Example:
const [query, setQuery] = useState(‘’);
const [results, setResults] = useState();useEffect(() => {
if (!query) {
setResults();
return;
}
const controller = new AbortController();
fetch(/api/search?q=${encodeURIComponent(query)}, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== ‘AbortError’) console.error(err);
});
return () => controller.abort();
}, [query]); -
On input change, call setQuery(e.target.value).
-
Do not mutate results directly, always use setResults.
• Debounce logic bugs
If you use a debounce, check the dependency array. Many people put an empty array so the effect never re-runs. The dependency must include query.
• Key prop issues
If list items use index as key, React sometimes does not re-render as you expect. Use movie.id as key.
- Favorites not saving between sessions
Decide where you store favorites:
A) LocalStorage approach
-
On change:
const [favorites, setFavorites] = useState(() => {
try {
const stored = localStorage.getItem(‘favorites’);
return stored ? JSON.parse(stored) : ;
} catch (e) {
console.error(e);
return ;
}
});useEffect(() => {
try {
localStorage.setItem(‘favorites’, JSON.stringify(favorites));
} catch (e) {
console.error(e);
}
}, [favorites]); -
When you toggle favorite, update the array through setFavorites, do not mutate in place.
setFavorites(prev => {
const exists = prev.find(m => m.id === movie.id);
if (exists) {
return prev.filter(m => m.id !== movie.id);
}
return […prev, movie];
});
Common bugs:
• Calling localStorage.setItem before state updates, then overwriting with old data.
• Storing different shapes, like sometimes ids, sometimes full objects. Then your equality check fails.
• Trying to access localStorage on server side if you use Next.js. Wrap in typeof window !== 'undefined'.
B) Backend storage
If you use an API to save favorites:
-
Check the network tab. See if the POST / DELETE calls succeed.
-
On success, sync UI immediately instead of waiting for a refetch:
const toggleFavorite = async movie => {
const exists = favorites.find(m => m.id === movie.id);
if (exists) {
setFavorites(prev => prev.filter(m => m.id !== movie.id));
await fetch(/api/favorites/${movie.id}, { method: ‘DELETE’ });
} else {
setFavorites(prev => […prev, movie]);
await fetch(/api/favorites, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ movieId: movie.id })
});
}
}; -
On app load, fetch favorites once and hydrate state.
- Quick debugging checklist
• Log your search query inside the effect or handler.
console.log(‘query’, query) before you call the API.
• Log the data you read from localStorage on startup.
console.log(‘stored favorites’, stored);
• Confirm that favorites load before you render the main UI, or handle the loading state.
• If results do not update until a second keystroke, you might read from event.target.value after async work. Always grab the value at top of the handler.
Small example with both pieces tied together:
-
On search input change:
const handleSearchChange = e => {
const value = e.target.value;
setQuery(value);
}; -
On star button click:
const isFavorite = favorites.some(m => m.id === movie.id);
<button onClick={() => toggleFavorite(movie)}>
{isFavorite ? ‘Unfavorite’ : ‘Favorite’}
If you post your search input component and your favorites state snippet, people can point to the exact bug.
Couple more angles you can check that complement what @cazadordeestrellas said, without rehashing the same hooks logic:
1) Search results not updating
You say API calls look fine, so I’d look at where the data is wired into the UI:
a) Derived state issues
If you do something like:
const [movies, setMovies] = useState([]);
const [results, setResults] = useState([]);
useEffect(() => {
// filter from movies into results
const filtered = movies.filter(m => m.title.includes(query));
setResults(filtered);
}, [query]);
and also update movies somewhere else, you can easily get desynced data because you’re maintaining 2 separate sources of truth.
Try using a single source of truth:
const [movies, setMovies] = useState([]);
const visibleMovies = useMemo(
() => movies.filter(m => m.title.toLowerCase().includes(query.toLowerCase())),
[movies, query]
);
Then render visibleMovies directly. No second state, fewer bugs.
b) Component-level vs global state
If query lives in a parent, but the actual list lives in a child that never re-renders, you’ll get “stuck” results.
Quick check: add console.log('render results', query) right inside the component that maps over the movies. If the query logs correctly while typing but the list doesn’t change, your render is using a stale prop or memo.
Suspicious patterns:
const MovieList = React.memo(function MovieList({ movies }) { ... });
If you memoize but pass a movies array that you mutate in place, React will think nothing changed. Make sure you always create a new array.
c) Input handler timing bugs
Common subtle bug:
const handleChange = (e) => {
setQuery(e.target.value);
doSearch(query); // query is the OLD value here
};
Then it always feels “one character behind.” If you’re manually calling a search function, feed it the new value:
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
doSearch(value);
};
Or rely purely on an effect with query in the dep array and don’t call doSearch there at all.
2) Favorites not persisting between sessions
Assuming your fetches are fine, I’d check when you read and write the favorites.
a) Initialization race conditions
If you do something like:
const [favorites, setFavorites] = useState([]);
useEffect(() => {
// hydrate from storage
const stored = JSON.parse(localStorage.getItem('favorites') || '[]');
setFavorites(stored);
}, []);
useEffect(() => {
localStorage.setItem('favorites', JSON.stringify(favorites));
}, [favorites]);
and somewhere else you set favorites to [] before the hydrate effect runs, you may overwrite the saved list on first load.
Look for patterns like:
useEffect(() => {
setFavorites([]); // e.g. when user logs out, or on some 'reset'
}, [/* something that runs on mount */]);
That can nuke what you just loaded.
b) Identity mismatch
If you save ids but compare whole objects, your “toggle favorite” may fail across sessions:
- On first run you store
{ id: 123, title: 'Foo' }. - On next run you fetch from API, get
{ id: 123, title: 'Foo' }but as a different object reference. - If your check is
favorites.includes(movie)that will be false after reload.
Use an id-based check only:
const isFavorite = favorites.some(f => f.id === movie.id);
and keep your stored data shape consistent: either only ids or full movie objects, not a mix.
c) Serialization pitfalls
If any of the movie data is not serializable to JSON cleanly, you can get silent failures or broken parses. Quick sanity test:
const raw = localStorage.getItem('favorites');
try {
const parsed = JSON.parse(raw);
console.log('favorites parsed OK', parsed);
} catch (e) {
console.log('favorites parse failed', raw);
}
If you see something like [object Object] or weird nested strings, you’re double-stringifying somewhere:
localStorage.setItem('favorites', JSON.stringify(JSON.stringify(favorites)));
then later JSON.parse only once, and get a string instead of an array.
3) App structure checks
Couple architectural checks that often cause both your problems at once:
a) Mixing in-memory cache and storage
If you keep some sort of “movies cache” and also restore favorites from storage, but the cache loads later, your UI might:
- Load favorites from storage.
- Render nothing because movies list is still empty.
- When movies load, recompute the “favorite” flags from only the in-memory data, ignoring what you hydrated.
Pattern that helps:
- Load favorites first from localStorage.
- Then load movies.
- While rendering movies, use the ids from favorites to flag them.
b) Conditional rendering killing state
If your favorites list component is conditionally rendered on route or tab changes, its local state is destroyed each time:
{showFavorites && <FavoritesPanel />}
If FavoritesPanel has its own useState for favorites plus localStorage sync, toggling showFavorites can re-init from empty or old storage.
Move favorites state up a level, so mount/unmount does not reset it.
If you can paste the component where:
- you handle the search input, and
- you toggle favorites and read from storage
you’ll probably get a very specific “here’s the bug” answer. The behavior you describe sounds like at least one stale state usage and one storage overwrite on initial load.