mapStateToProps is undefined - javascript

I want to fetch data from json file and then render it on the screen with React and Redux. JSX is standart, I used the <Provide> tag and set store value to my store. mapStateToProps is going undefined for this.props as well as toTakeData().
Here I have action file with request:
let data = {
loading: true,
items: [],
prevName: null,
selectedProfile: '',
term: ''
}
export function getItems() {
getRequest();
return {
type: 'GET_ITEMS',
payload: data
}
}
const getRequest = async () => {
const response = await fetch('http://localhost:8000/api/item')
.then( response => response.json() )
.then( json => {
data.items = json;
data.selectedProfile = json[0];
data.loading = false;
data.prevName = json[0].general.firstName + ' ' + json[0].general.lastName;
} )
.catch( err => console.error( err ) );
}
And here is component file which suppose to render data:
const mapStateToProps = state => {
console.log(state.items);
return {
items: state.items,
prevName: state.prevName,
selectedProfile: state.selectedProfile,
term: state.term,
loading: state.loading
};
};
const mapActionsToProps = {
toTakeData: getItems
};
export default connect(mapStateToProps, mapActionsToProps)(SelectMenu);

Related

Redux actions always in pending state

I am trying to create a scraping application using redux toolkit for learning purposes.Whenever I dispatch the action the data gets scraped and console logged but the action state is never fullfilled and is always pending
MY ASYNC THUNK
export const loadData = createAsyncThunk(
"alldata/getdata",
async ({ pageNo, language }, thunkAPI) => {
const data = await fetch(
`http://localhost:5000/scrape?pageNo=${encodeURIComponent(
pageNo
)}&language=${encodeURIComponent(language)}`
);
const res=await data.json()
return {
payload: res,
};
}
);
MY SLICE
const projectSlice = createSlice({
name: "allprojects",
initialState: {
projectState: [],
workingState: [],
isLoading: false,
hasError: false,
},
reducers: {
addProject: (state, action) => {
return state.workingState.push(action.payload);
},
removeProject: (state, action) => {
return state.workingState.filter(
(project) => project.link !== action.payload.link
);
},
},
extraReducers: {
[loadData.pending]: (state, action) => {
state.isLoading = true;
state.hasError = false;
},
[loadData.fulfilled]: (state, { payload }) => {
state.projectState = payload;
state.isLoading = false;
state.hasError = false;
},
[loadData.rejected]: (state, action) => {
state.isLoading = false;
state.hasError = true;
},
},
});
export const { addProject, removeProject } = projectSlice.actions;
const Projectreducer = projectSlice.reducer;
export default Projectreducer;
export const projectSelector = (state) => state.allprojects;
REACT COMPONENT
const { workingState, projectState, isLoading, hasError } =
useSelector(projectSelector);
const dispatch = useDispatch();
const [selectData, setSelectData] = React.useState({ languages: "" });
const [pageData, setPageData] = React.useState({ pageNo: 1 });
const handleClick = (event) => {
event.preventDefault();
dispatch(
loadData({ pageNo: pageData.pageNo, language: selectData.languages })
);
};
So how do I get the action to be fullfilled and push the data in the ProjectState array after the async request
EDIT:
API
app.get("/scrape", async (req, res) => {
const { pageNo, language } = req.query;
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(
`https://github.com/search?p=${pageNo}&q=language%3A${language}`,
{
waitUntil: "domcontentloaded",
}
); // URL is given by the "user" (your client-side application)
const data = await page.evaluate(() => {
const list = [];
const items = document.querySelectorAll(".repo-list-item");
for (const item of items) {
list.push({
projectName: item.querySelector(".f4 > a").innerText,
about: item.querySelector("p").innerText,
link: item.querySelector("a").getAttribute("href"),
});
}
return list;
});
console.log(data);
await browser.close();
});
Store
import { configureStore } from "#reduxjs/toolkit";
import Projectreducer from "./Slices/slice";
export const store = configureStore({
reducer: {
allprojects: Projectreducer,
},
});
Its possible that the api that you are fetching is throwing an error so in this case it always recommended to have a catch block and throw an error to that its falls into loadData.rejected state.
So, do the check network tab in the dev tools of the browser that you are using, so that you can confirm if the api is responding or not.
Also can you share the projectSelector selector ? could be the something wrong in the selector.
action:
export const loadData = createAsyncThunk(
'alldata/getdata',
async ({ pageNo, language }, { rejectWithValue }) => {
try {
const data = await fetch(
`http://localhost:5000/scrape?pageNo=${encodeURIComponent(
pageNo
)}&language=${encodeURIComponent(language)}`
);
const res = await data.json();
return {
payload: res,
};
} catch (error) {
return rejectWithValue({ payload: error?.message || error });
}
}
);
reducer:
extraReducers: {
...,
[loadData.rejected]: (state, { payload }) => {
state.isLoading = false;
state.hasError = true;
state.message = payload;
},
},
One more thing to mention here is that redux toolkit recommends to use builder callback, for more details check here:
https://redux-toolkit.js.org/api/createslice#extrareducers
in your example it would be like
extraReducers: (builder) => {
builder
.addCase(loadData.pending, (state) => {
...
})
.addCase(loadData.fulfilled, (state, action) => {
...
})
.addCase(loadData.rejected, (state, action) => {
...
});

react-query 3 setQueryData doesn't update the cache

I have this query:
export function useCardList() {
return useQuery(['cardList'], fetchCardList());
}
I'm using it in my component:
const { data, isLoading } = useCardList();
// delete
const handleOnDelete = (id: string) => {
const newData = {
...data,
data: data?.data.filter((card) => card.id !== id)
};
queryClient.setQueryData(['cardList'], newData);
};
but setQueryData doesn't work. what do you think? (edited)

When routing mswjs/data populates the database with new items and removes the previous one, making it inaccessible

I use next-redux-wrapper, MSW, #mswjs/data and redux-toolkit for storing my data in a store as well as mocking API calls and fetching from a mock Database.
I have the following scenario happening to me.
I am on page /content/editor and in the console and terminal, I can see the data was fetched from the mock database and hydrated from getStaticProps of Editor.js. So now IDs 1 to 6 are inside the store accessible.
Now I click on the PLUS icon to create a new project. I fill out the dialog and press "SAVE". a POST request starts, it's pending and then it gets fulfilled. The new project is now in the mock DB as well as in the store, I can see IDs 1 to 7 now.
Since I clicked "SAVE" and the POST request was successful, I am being routed to /content/editor/7 to view the newly created project.
Now I am on Page [id].js, which also fetched data from the mock DB and then it gets stored and hydrated into the redux store. The idea is, it takes the previous store's state and spreads it into the store, with the new data (if there are any).
Now the ID 7 no longer exists. And IDs 1 to 6 also don't exist anymore, instead, I can see in the console and terminal that IDs 8 to 13 were created, and the previous ones are no more.
Obviously, this is not great. When I create a new project and then switch the route, I should be able to access the newly created project as well as the previously created ones. But instead, they all get overwritten.
It either has something to do with the next-redux-wrapper or MSW, but I am not sure how to make it work. I need help with it. I will post some code now:
Code
getStaticProps
// path example: /content/editor
// Editor.js
export const getStaticProps = wrapper.getStaticProps(
(store) =>
async ({ locale }) => {
const [translation] = await Promise.all([
serverSideTranslations(locale, ['editor', 'common', 'thesis']),
store.dispatch(fetchProjects()),
store.dispatch(fetchBuildingBlocks()),
]);
return {
props: {
...translation,
},
};
}
);
// path example: /content/editor/2
// [id].js
export const getStaticProps = wrapper.getStaticProps(
(store) =>
async ({ locale, params }) => {
const { id } = params;
const [translation] = await Promise.all([
serverSideTranslations(locale, ['editor', 'common', 'thesis']),
store.dispatch(fetchProjects()),
// store.dispatch(fetchProjectById(id)), // issue: fetching by ID returns null
store.dispatch(fetchBuildingBlocks()),
]);
return {
props: {
...translation,
id,
},
};
}
);
Mock Database
Factory
I am going to shorten the code to the relevant bits. I will remove properties for a project, as well es helper functions to generate data.
const asscendingId = (() => {
let id = 1;
return () => id++;
})();
const isDevelopment =
process.env.NODE_ENV === 'development' || process.env.STORYBOOK || false;
export const projectFactory = () => {
return {
id: primaryKey(isDevelopment ? asscendingId : nanoid),
name: String,
// ... other properties
}
};
export const createProject = (data) => {
return {
name: data.name,
createdAt: getUnixTime(new Date()),
...data,
};
};
/**
* Create initial set of tasks
*/
export function generateMockProjects(amount) {
const projects = [];
for (let i = amount; i >= 0; i--) {
const project = createProject({
name: faker.lorem.sentence(faker.datatype.number({ min: 1, max: 5 })),
dueDate: date(),
fontFamily: getRandomFontFamily(),
pageMargins: getRandomPageMargins(),
textAlign: getRandomTextAlign(),
pageNumberPosition: getRandomPageNumberPosition(),
...createWordsCounter(),
});
projects.push(project);
}
return projects;
}
API Handler
I will shorten this one to GET and POST requests only.
import { db } from '../../db';
export const projectsHandlers = (delay = 0) => {
return [
rest.get('https://my.backend/mock/projects', getAllProjects(delay)),
rest.get('https://my.backend/mock/projects/:id', getProjectById(delay)),
rest.get('https://my.backend/mock/projectsNames', getProjectsNames(delay)),
rest.get(
'https://my.backend/mock/projects/name/:id',
getProjectsNamesById(delay)
),
rest.post('https://my.backend/mock/projects', postProject(delay)),
rest.patch(
'https://my.backend/mock/projects/:id',
updateProjectById(delay)
),
];
};
function getAllProjects(delay) {
return (request, response, context) => {
const projects = db.project.getAll();
return response(context.delay(delay), context.json(projects));
};
}
function postProject(delay) {
return (request, response, context) => {
const { body } = request;
if (body.content === 'error') {
return response(
context.delay(delay),
context.status(500),
context.json('Server error saving this project')
);
}
const now = getUnixTime(new Date());
const project = db.project.create({
...body,
createdAt: now,
maxWords: 10_000,
minWords: 7000,
targetWords: 8500,
potentialWords: 1500,
currentWords: 0,
});
return response(context.delay(delay), context.json(project));
};
}
// all handlers
import { buildingBlocksHandlers } from './api/buildingblocks';
import { checklistHandlers } from './api/checklist';
import { paragraphsHandlers } from './api/paragraphs';
import { projectsHandlers } from './api/projects';
import { tasksHandlers } from './api/tasks';
const ARTIFICIAL_DELAY_MS = 2000;
export const handlers = [
...tasksHandlers(ARTIFICIAL_DELAY_MS),
...checklistHandlers(ARTIFICIAL_DELAY_MS),
...projectsHandlers(ARTIFICIAL_DELAY_MS),
...buildingBlocksHandlers(ARTIFICIAL_DELAY_MS),
...paragraphsHandlers(ARTIFICIAL_DELAY_MS),
];
// database
import { factory } from '#mswjs/data';
import {
buildingBlockFactory,
generateMockBuildingBlocks,
} from './factory/buildingblocks.factory';
import {
checklistFactory,
generateMockChecklist,
} from './factory/checklist.factory';
import { paragraphFactory } from './factory/paragraph.factory';
import {
projectFactory,
generateMockProjects,
} from './factory/project.factory';
import { taskFactory, generateMockTasks } from './factory/task.factory';
export const db = factory({
task: taskFactory(),
checklist: checklistFactory(),
project: projectFactory(),
buildingBlock: buildingBlockFactory(),
paragraph: paragraphFactory(),
});
generateMockProjects(5).map((project) => db.project.create(project));
const projectIds = db.project.getAll().map((project) => project.id);
generateMockTasks(20, projectIds).map((task) => db.task.create(task));
generateMockBuildingBlocks(10, projectIds).map((block) =>
db.buildingBlock.create(block)
);
const taskIds = db.task.getAll().map((task) => task.id);
generateMockChecklist(20, taskIds).map((item) => db.checklist.create(item));
Project Slice
I will shorten this one as well to the relevant snippets.
// projects.slice.js
import {
createAsyncThunk,
createEntityAdapter,
createSelector,
createSlice,
current,
} from '#reduxjs/toolkit';
import { client } from 'mocks/client';
import { HYDRATE } from 'next-redux-wrapper';
const projectsAdapter = createEntityAdapter();
const initialState = projectsAdapter.getInitialState({
status: 'idle',
filter: { type: null, value: null },
statuses: {},
});
export const fetchProjects = createAsyncThunk(
'projects/fetchProjects',
async () => {
const response = await client.get('https://my.backend/mock/projects');
return response.data;
}
);
export const saveNewProject = createAsyncThunk(
'projects/saveNewProject',
async (data) => {
const response = await client.post('https://my.backend/mock/projects', {
...data,
});
return response.data;
}
);
export const projectSlice = createSlice({
name: 'projects',
initialState,
reducers: {
// irrelevant reducers....
},
extraReducers: (builder) => {
builder
.addCase(HYDRATE, (state, action) => {
// eslint-disable-next-line no-console
console.log('HYDRATE', action.payload);
const statuses = Object.fromEntries(
action.payload.projects.ids.map((id) => [id, 'idle'])
);
return {
...state,
...action.payload.projects,
statuses,
};
})
.addCase(fetchProjects.pending, (state, action) => {
state.status = 'loading';
})
.addCase(fetchProjects.fulfilled, (state, action) => {
projectsAdapter.addMany(state, action.payload);
state.status = 'idle';
action.payload.forEach((item) => {
state.statuses[item.id] = 'idle';
});
})
.addCase(saveNewProject.pending, (state, action) => {
console.log('SAVE NEW PROJECT PENDING', action);
})
.addCase(saveNewProject.fulfilled, (state, action) => {
projectsAdapter.addOne(state, action.payload);
console.group('SAVE NEW PROJECT FULFILLED');
console.log(current(state));
console.log(action);
console.groupEnd();
state.statuses[action.payload.id] = 'idle';
})
// other irrelevant reducers...
},
});
This should be all the relevant code. If you have questions, please ask them and I will try to answer them.
I have changed how the state gets hydrated, so I turned this code:
.addCase(HYDRATE, (state, action) => {
// eslint-disable-next-line no-console
console.log('HYDRATE', action.payload);
const statuses = Object.fromEntries(
action.payload.projects.ids.map((id) => [id, 'idle'])
);
return {
...state,
...action.payload.projects,
statuses,
};
})
Into this code:
.addCase(HYDRATE, (state, action) => {
// eslint-disable-next-line no-console
console.group('HYDRATE', action.payload);
const statuses = Object.fromEntries(
action.payload.projects.ids.map((id) => [id, 'idle'])
);
state.statuses = { ...state.statuses, ...statuses };
projectsAdapter.upsertMany(state, action.payload.projects.entities);
})
I used the adapter to upsert all entries.

How to make an HTTP hook reusable on the same component

I have an HTTP hook that can be consumed like this:
const { data, error, isLoading, executeFetch } = useHttp<IArticle[]>('news', []);
In the same component, I want to trigger another API call to POST data and update one of the articles:
const handleChange = (article: IArticle, event: React.ChangeEvent<HTMLInputElement>) => {
executeFetch(`updateNews?id=${article.id}`, { method: 'post', data: { isRead: event.target.checked }});
};
return (
<>
<div className={classes.articleListHeader}>
<h1>Article List</h1>
<small className={classes.headerSubtitle}>{data.length} Articles</small>
</div>
<ul>
{data.map(article => <Article key={article.id} article={article} handleChange={handleChange}/>)}
</ul>
</>
)
My custom hook to fetch data:
export function useHttp<T>(initUrl: string, initData: T): UseHttp<T> {
const initOptions: AxiosRequestConfig = { url: initUrl };
const [options, setOptions] = useState(initOptions);
const useHttpReducer = createHttpReducer<T>();
const [state, dispatch] = useReducer(useHttpReducer, {
isLoading: false,
error: '',
data: initData
});
useEffect(() => {
let cancelRequest = false;
const fetchData = async (cancelRequest: boolean = false) => {
if (!options.url) return;
dispatch({ type: API_REQUEST});
try {
const responsePromise: AxiosPromise<T> = axios(options);
const response = await responsePromise;
if (cancelRequest) return;
dispatch({ type: API_SUCCESS, payload: response.data });
} catch (e) {
console.log("Got error", e);
dispatch({ type: API_ERROR, payload: e.message });
}
};
fetchData(cancelRequest);
return () => {
cancelRequest = true;
}
}, [options]);
const executeFetch = (url: string, options: AxiosRequestConfig = axiosInitialOptions): void => {
options.url = url;
setOptions(options);
};
return { ...state, executeFetch}
The issue is, when I'm doing something like this, the data replaces to the new response (of the POST request), then my UI crashes (no more article list..)
What's the good practice to manage situations like this when I need to call another API in the same component while keeping the reusability of my HTTP hook?
I simply want to execute a POST request somewhere in the component after my GET one - How I can do it in a reusable way and fix my issue?
You can refactoring your custom hook to receive a callback function. I omitted the part of cancelRequest, if you are using axios you can cancel the request via CancelToken:
export function useHttp<T>(initUrl: string): UseHttp<T> {
const initOptions: AxiosRequestConfig = { url: initUrl };
const [options, setOptions] = useState(initOptions);
const useHttpReducer = createHttpReducer<T>();
const [state, dispatch] = useReducer(useHttpReducer, {
isLoading: false,
error: '',
});
const fetchData = async (options, callback) => {
if (!options.url) return;
dispatch({ type: API_REQUEST});
try {
const responsePromise: AxiosPromise<T> = axios(options);
const response = await responsePromise;
dispatch({ type: API_SUCCESS, payload: response.data });
callback(response.data);
} catch (e) {
console.log("Got error", e);
dispatch({ type: API_ERROR, payload: e.message });
}
};
const executeFetch = (url: string, requestOptions: AxiosRequestConfig = axiosInitialOptions, callback): void => {
options.url = url;
fetchData({...options, ...requestOptions}, callback);
};
return { ...state, executeFetch}
};
Usage:
const [articles, setArticles]= useState();
const { error, isLoading, executeFetch } = useHttpRequest();
const handleChange = (article: IArticle, event: React.ChangeEvent<HTMLInputElement>) => {
executeFetch(`updateNews?id=${article.id}`, { method: 'post', data: { isRead: event.target.checked }}, setArticles);
};

how can i pass data by using react navigation in react-native?

I have a NotifiCard component and a ReplyComment component
When I run the LookforReply function, the GETONECOMMENT_REQUEST and LOAD_POST_REQUEST dispatches are executed, and the data comes into onecomment.
which is
const {onecomment} = useSelector((state) => state.post);
And I also want to pass the {item:one comment} data in the ReplyComment using navigation.navigate.
However, when we run our code, the data does not come into ReplyComment immediately, so it causing an error.
this is my code
(NotifiCard.js)
const NotifiCard = ({item}) => {
const dispatch = useDispatch();
const navigation = useNavigation();
const {onecomment} = useSelector((state) => state.post);
const LookforReply = useCallback(() => {
dispatch({
type:GETONECOMMENT_REQUEST,
data:item?.CommentId,
}),
dispatch({
type: LOAD_POST_REQUEST,
data:item.PostId,
}),
navigation.navigate('ReplyComment',{itemm:onecomment})
},[]);
return (
<LookContainer onPress={LookforReply}>
<Label>대댓보기</Label>
</LookContainer>
);
};
when dispatch GETONECOMMENT_REQUEST, getonecommentAPI this api run and get data from backend router
(postsaga.js)
function getonecommentAPI(data) {
return axios.get(`/post/${data}/getonecomment`);
}
function* getonecomment(action) {
try {
const result = yield call(getonecommentAPI, action.data);
yield put({
type: GETONECOMMENT_SUCCESS,
data: result.data,
});
} catch (err) {
console.error(err);
yield put({
type: GETONECOMMENT_FAILURE,
error: err.response.data,
});
}
}
which is this backend router
(backend/post.js)
router.get('/:onecommentId/getonecomment', async (req, res, next) => {
try {
// console.log("req.params.onecomment:",req.params.onecommentId);
const onecomment = await Comment.findOne({
where:{id: req.params.onecommentId},
include: [{
model: User,
attributes: ['id', 'nickname'],
}],
})
// console.log("onecomment:",JSON.stringify(onecomment));
res.status(200).json(onecomment);
} catch (error) {
console.error(error);
next(error);
}
});
if i get result data this will put draft.onecomment
(reducer/post.js)
case GETONECOMMENT_REQUEST:
draft.loadPostLoading = true;
draft.loadPostDone = false;
draft.loadPostError = null;
break;
case GETONECOMMENT_SUCCESS:
// console.log("action.data:::",action.data);
draft.loadPostLoading = false;
draft.onecomment = action.data;
draft.loadPostDone = true;
break;
case GETONECOMMENT_FAILURE:
draft.loadPostLoading = false;
draft.loadPostError = action.error;
break;
and i can get data in onecomment by using useselector at (NotifiCard.js)
const {onecomment} = useSelector((state) => state.post);
what i want is that when i press LookforReply i want to pass itemm data to
ReplyComment component but if i press LookforReply, i can't get itemm data
Immediately
(ReplyComment.js)
const ReplyComment = ({route}) => {
const {itemm} = route.params;
console.log("itemm",itemm);
return (
<Container>
<TodoListView parenteitem={itemm} />
<AddTodo item={itemm} />
</Container>
);
};
I think that the LookforReply function is executed asynchronously and navigate is executed before the onecomment data comes in, and it doesn't seem to be able to deliver the itemm.
so how can i fix my code?....

Categories

Resources