I have defined a Redux Toolkit slice within a file, searchSlice.js
, which is responsible for querying an API and storing the response data in the store's state. It currently looks like this:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const initialState = {
query: '',
status: 'idle',
movies: [],
totalResults: null,
};
// Create slice
export const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {
updateQuery: (state, action) => {
state.query = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(getMovies.pending, (state) => {
state.status = 'pending';
})
.addCase(getMovies.fulfilled, (state, action) => {
state.status = 'idle';
state.movies = action.payload.results;
state.totalResults = action.payload.total_results;
});
},
});
// Actions
export const { updateQuery } = searchSlice.actions;
// Reducers
export default searchSlice.reducer;
// Selectors
export const selectQuery = (state) => state.search.query;
export const selectStatus = (state) => state.search.status;
export const selectAllMovies = (state) => state.search.movies;
export const selectTotalResults = (state) => state.search.totalResults;
// Thunks
export const getMovies = createAsyncThunk(
'search/getMovies',
async (payload, store) => {
if (!store.getState().search) {
dispatchEvent(updateQuery(payload));
}
try {
console.log('payload: ', payload);
const res = await axios.get(`/search?query=${store.getState().search}`);
return res.data;
} catch (err) {
return err;
}
}
);
As I see it, besides exporting the actual slice object itself, you also must export its necessary and accompanying components:
Due to the nature of the aforementioned components being highly coupled and interdependent, ESLint will throw different linting errors based on the order of the components within the searchSlice.js
file, by line number. For example, in the above code snippet, the linting error is:
'getMovies' was used before it was defined. eslint(no-use-before-define)
And if we attempt to fix the error by rearranging the getMovies
function declaration to above the invocation, like so:
// ...
// Thunks
export const getMovies = createAsyncThunk(
'search/getMovies',
async (payload, store) => {
if (!store.getState().search) {
dispatchEvent(updateQuery(payload));
}
try {
console.log('payload: ', payload);
const res = await axios.get(`/search?query=${store.getState().search}`);
return res.data;
} catch (err) {
return err;
}
}
);
// ...
// Create slice
export const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {
updateQuery: (state, action) => {
state.query = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(getMovies.pending, (state) => {
state.status = 'pending';
})
.addCase(getMovies.fulfilled, (state, action) => {
state.status = 'idle';
state.movies = action.payload.results;
state.totalResults = action.payload.total_results;
});
},
});
// ...
Then we get the same linting error, but with different function definitions:
'updateQuery' was used before it was defined. eslint(no-use-before-define)
It seems like regardless of the file's arrangement, ESLint will throw a paradoxical no-use-before-define
error.
Is there a solution to this that doesn't involve changing the ESLint rules? Is there a better way to structure the code? I have already tried splitting it up into smaller files, but due to the highly interdependent nature of accompanying slice functionality, ESLint will start throwing import/no-cycle
errors, because both files need to import stuff from eachother.
As an additional question, how does hoisting come into play here?
When you are dealing with a cycle you need to figure out which makes sense as the dependency and which is the dependent. In this case it is easier to remove the reducer from the thunk than it is to remove the thunk from the reducer.
You can fix the dependency by removing the additional dispatched updateQuery
action from the thunk and handling that logic in your reducer instead. You can access the (confusingly named) payload
variable from your thunk in the getMovies.pending
case reducer through the action.meta.arg
property, which contains the argument that you called the thunk action creator with.
export const getMovies = createAsyncThunk(
'search/getMovies',
async (query) => {
const res = await axios.get(`/search?query=${query}`);
return res.data;
// Don't catch errors here. Let them be thrown and handled by the 'rejected' action.
}
);
export const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {
// you might not even need this anymore, unless you use it elsewhere.
updateQuery: (state, action) => {
state.query = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(getMovies.pending, (state, action) => {
// Update the query property of the state.
state.query = action.meta.arg;
state.status = 'pending';
})
.addCase(getMovies.fulfilled, (state, action) => {
state.status = 'idle';
state.movies = action.payload.results;
state.totalResults = action.payload.total_results;
});
},
});
By the way, the conditional if (!store.getState().search)
does not make sense. That would be checking if the entire slice is truthy, which it always is.