I'm using Jest and React-testing-library to test a React component. I've rendered a component with a Context API Provider so the component has access to the Context. Unfortunately, I don't know how could I access it from outside the rendered component. In this case there is an isUserLogged property in the Context depending on which the component changes. But I don't know how could I change the value of this property. How could I approach this case? If mocking of the Context API required how could I do it?
In the attached code I try to access a Context using a useContext hook. The error says I can't use hooks outside of React components. And, obviously, I'm trying to access the Context outside the Context Provider so it wouldn't work anyway.
import { Provider, Context } from '../../context';
test('Star is not clickable for a non-registered user', () => {
const { getByTestId } = render(<Provider><Star /></Provider>);
const isUserLogged = useContext(Context).isUserLogged; // Error.
});
You simply have to render your Provider passing a value prop. That's how you would do it in your app.
const isUserLogged = true
render(<Provider value={{ isUserLogged }}><Star /></Provider>)
This is how I access to context in order to initialise state to be ready for a function being tested:
let authContext
const TestWrapper = ({ children }) => {
authContext = useContext(AuthContext)
return children
}
const renderer = renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider>
<TestWrapper>{children}</TestWrapper>
</AuthProvider>
),
})
act(() => {
authContext.setUser(user)
})
// hook that uses the same context now have 'user' initialised
Related
I am trying to store the button click logs user level so that I can use them somewhere in my project can you help me and tell how I can do that thing user level and set them using an api and get them when ever needed I am using svelte for my front end (store the log using firebase or something similar).user level means for each user different data will be saved and and I can use userid to get the details for the user by using userid
I suggest some decorator pattern applied to the event handler of interest.
Suppose you have a button and you want to log the on:click event. You can decorate the handleClick event handler with a decorator named monitor.
<script>
import { getContext, onMount } from "svelte"
const monitor = getContext("monitor")
function handleClick(event) {
// do stuff
}
</script>
<button on:click={monitor(handleClick)}>Click me</button>
Now in your root <App /> component, you should initialize monitor decorator and set it into context.
<script> // App.svelte
import { setContext } from "svelte"
import { Login } from "./components/Login.svelte"
let userId = null
const setUserId = (id) => { userId = id }
// the decorator wraps the original eventHandler
// and smuggles in some report logic
function monitor(eventHandler) {
return function decoratedEventHandler() {
// if we have the userId, we can start to report event logs
if (userId) {
const event = arguments[0]
// impl report as you needed
// it can simply be a HTTP POST to some backend API
// I definitely suggest add some throttle mechanism into it
report(userId, event)
}
eventHandler.apply(this, arguments)
}
}
setContext("monitor", monitor)
onMount(() => {
// if you store userId in cookie,
// you parse the cookie and restore it
userId = getUserIdFromCookie(document.cookie)
})
</script>
<!--
or maybe your Login component has some special logic
to get userId from backend
-->
<Login updateUserId={setUserId} ></Login>
I'm new to Next.js and I'm trying to understand the suggested structure and dealing with data between pages or components.
For instance, inside my page home.js, I fetch an internal API called /api/user.js which returns some user data from MongoDB. I am doing this by using fetch() to call the API route from within getServerSideProps(), which passes various props to the page after some calculations.
From my understanding, this is good for SEO, since props get fetched/modified server-side and the page gets them ready to render. But then I read in the Next.js documentation that you should not use fetch() to all an API route in getServerSideProps(). So what am I suppose to do to comply to good practice and good SEO?
The reason I'm not doing the required calculations for home.js in the API route itself is that I need more generic data from this API route, as I will use it in other pages as well.
I also have to consider caching, which client-side is very straightforward using SWR to fetch an internal API, but server-side I'm not yet sure how to achieve it.
home.js:
export default function Page({ prop1, prop2, prop3 }) {
// render etc.
}
export async function getServerSideProps(context) {
const session = await getSession(context)
let data = null
var aArray = [], bArray = [], cArray = []
const { db } = await connectToDatabase()
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
if (session) {
const hostname = process.env.NEXT_PUBLIC_SITE_URL
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/user`, options)
const json = await res.json()
if (json.data) { data = json.data }
// do some math with data ...
// connect to MongoDB and do some comparisons, etc.
But then I read in the Next.js documentation that you should not use fetch() to all an API route in getServerSideProps().
You want to use the logic that's in your API route directly in getServerSideProps, rather than calling your internal API. That's because getServerSideProps runs on the server just like the API routes (making a request from the server to the server itself would be pointless). You can read from the filesystem or access a database directly from getServerSideProps. Note that this only applies to calls to internal API routes - it's perfectly fine to call external APIs from getServerSideProps.
From Next.js getServerSideProps documentation:
It can be tempting to reach for an API Route when you want to fetch
data from the server, then call that API route from
getServerSideProps. This is an unnecessary and inefficient approach,
as it will cause an extra request to be made due to both
getServerSideProps and API Routes running on the server.
(...) Instead, directly import the logic used inside your API Route
into getServerSideProps. This could mean calling a CMS, database, or
other API directly from inside getServerSideProps.
(Note that the same applies when using getStaticProps/getStaticPaths methods)
Here's a small refactor example that allows you to have logic from an API route reused in getServerSideProps.
Let's assume you have this simple API route.
// pages/api/user
export default async function handler(req, res) {
// Using a fetch here but could be any async operation to an external source
const response = await fetch(/* external API endpoint */)
const jsonData = await response.json()
res.status(200).json(jsonData)
}
You can extract the fetching logic to a separate function (can still keep it in api/user if you want), which is still usable in the API route.
// pages/api/user
export async function getData() {
const response = await fetch(/* external API endpoint */)
const jsonData = await response.json()
return jsonData
}
export default async function handler(req, res) {
const jsonData = await getData()
res.status(200).json(jsonData)
}
But also allows you to re-use the getData function in getServerSideProps.
// pages/home
import { getData } from './api/user'
//...
export async function getServerSideProps(context) {
const jsonData = await getData()
//...
}
You want to use the logic that's in your API route directly in
getServerSideProps, rather than calling your internal API. That's
because getServerSideProps runs on the server just like the API routes
(making a request from the server to the server itself would be
pointless). You can read from the filesystem or access a database
directly from getServerSideProps
As I admit, what you say is correct but problem still exist. Assume you have your backend written and your api's are secured so fetching out logic from a secured and written backend seems to be annoying and wasting time and energy. Another disadvantage is that by fetching out logic from backend you must rewrite your own code to handle errors and authenticate user's and validate user request's that exist in your written backend. I wonder if it's possible to call api's within nextjs without fetching out logic from middlewars? The answer is positive here is my solution:
npm i node-mocks-http
import httpMocks from "node-mocks-http";
import newsController from "./api/news/newsController";
import logger from "../middlewares/logger";
import dbConnectMid from "../middlewares/dbconnect";
import NewsCard from "../components/newsCard";
export default function Home({ news }) {
return (
<section>
<h2>Latest News</h2>
<NewsCard news={news} />
</section>
);
}
export async function getServerSideProps() {
let req = httpMocks.createRequest();
let res = httpMocks.createResponse();
async function callMids(req, res, index, ...mids) {
index = index || 0;
if (index <= mids.length - 1)
await mids[index](req, res, () => callMids(req, res, ++index, ...mids));
}
await callMids(
req,
res,
null,
dbConnectMid,
logger,
newsController.sendAllNews
);
return {
props: { news: res._getJSONData() },
};
}
important NOTE: don't forget to use await next() instead of next() if you use my code in all of your middlewares or else you get an error.
Another solution: next connect has run method that do something like mycode but personally I had some problems with it; here is its link:
next connet run method to call next api's in serverSideProps
Just try to use useSWR, example below
import useSWR from 'swr'
import React from 'react';
//important to return only result, not Promise
const fetcher = (url) => fetch(url).then((res) => res.json());
const Categories = () => {
//getting data and error
const { data, error } = useSWR('/api/category/getCategories', fetcher)
if (error) return <div>Failed to load</div>
if (!data) return <div>Loading...</div>
if (data){
// {data} is completed, it's ok!
//your code here to make something with {data}
return (
<div>
//something here, example {data.name}
</div>
)
}
}
export default Categories
Please notice, fetch only supports absolute URLs, it's why I don't like to use it.
P.S. According to the docs, you can even use useSWR with SSR.
Here I am fetching data for SSR and dispatch that data from Client.
const MyPage = ({
myFetch1,
myFetch2,
myFetch3,
}) => {
const dispatch = useDispatch();
dispatch(doSomething1(myFetch1));
dispatch(doSomething2(myFetch2));
dispatch(doSomething3(myFetch3));
return (
<Page>
<Head />
<MyOtherItem />
</Page>
);
};
MyPage.getInitialProps = async () => {
const myFetch1 = await myFetch1();
const myFetch2 = await myFetch2();
const myFetch3 = await myFetch3();
return {
myFetch1,
myFetch2,
myFetch3,
};
};
When i click on the link to route on this page
Issue:
The whole UI get freeze until fetching has done.
What is the best way to fetch that data
What is the best way to dispatch that data.
The main purpose of Server-side rendering is to fetch all data before populate it through page props for Front-end manipulation.
Although I found this concept pretty useful, since it prevents component re-rendering.
In case you really need to send your request on client side I would suggest you to use SWR hook which was developed by Next.js team:
For more info and code snippets, head to the following link:
https://github.com/vercel/swr
I have a node server that performs CRUD operations on data stored in a MongoDB Atlas database. My front-end is made using react where i am using redux for state management. Before I had the backend setup, I was initializing the default state of the redux store by calling a function i made which just returns JSON data. Now I want to make a get request to the server via axios to retrieve that same JSON data which is now on the Mongo database.
I know that I should be making axios get calls in componentDidMount lifecycle hook but my store.js is not a class but i'm not sure how. I am however able to just do axios.get(URL) but it returns an object in the form of [[PromiseStatus]]:"resolved", [[PromiseValue]]:{the data i want}. I read that these are not accessible. Im wondering if it is because I am not making the axios call in the right place or in the right time in the lifecycles.
import { createStore } from "redux";
//import { syncHistoryWithStore } from "react-router-redux";
//import { browserHistory } from "react-router-dom";
import axios from "axios";
//import the root reducer.
import rootReducer from "../reducers/rootReducer";
//this is just getting JSON from a file
import { getAnnotations } from "../services/fakeEntry";
//create an object for default data
const defaultState = {
//This is how i was creating the default state before.
// metadata: getAnnotations()
//this is what id like to do.But this needs to be in a lifecycle hook?
metadata: axios
.get("http://localhost:5100/data/")
.then(response => {
return response;
})
.catch(function(error) {
console.log(error);
})
};
const store = createStore(rootReducer, defaultState);
export default store;
The code above is the redux store. I have tested the endpoint using postman and it returns the JSON data. When i read this redux state using mapStateToProps in another component i get returned a Promise object.
I see two mistakes in your approach.
Redux is built from principles of functional programming and based on pure functions (without side effects, like API calls). So default state should some default empty object like below
const defaultState = {
metadata: {} // It should be empty during store init
isDataInitialized: false // You can add additional property to denote, that data is not fetched for the first time
}
axios returns Promise (as you already discovered). So take data from Promise, you should either user .then and set data in callback, or use async\await
Here is sample using async\await
// Now it is function, not object
const getInitalData = () => async dispatch => {
try {
let metadata = await axios
.get("http://localhost:5100/data/");
// You're dispatching not only the metadata, but also setting isDataInitialized to true, to denote, that data has been loaded
dispatch ({ type: 'DATA_INITIALIZED', metadata, isDataInitialized: true });
}
catch {
console.log(error);
}
}
Essentially getInitalData is action creator, which will dispatch action DATA_LOADED to store.
And you should create store with middleware (like thunk), to be able to dispatch actions which are functions.
const store = createStore(rootReducer, applyMiddleware(
thunkMiddleware
))
defaultState should go to reducer directly, like below
rootReducer (state = defaultState, action) {
// ...
Then, in some root component of your app, you call getInitalData to load data from server to store.
Here is simple sample you can use for better understanding
I am building an application which uses authorization with Json Web Tokens. I'm building this application with Node.js, GraphQL and Apollo client V2 (and some other stuff, but those aren't related here). I have created a login resolver and a currentUser resolver that let me get the current user via a JWT. I later use that token and send it in my authorization headers and the results looks something like:
So that part is done! But here's what I'm having trouble with.
Me trying to explain the situation
I'm using React for the frontend part of this project with the Apollo Client V2. And when I do the login mutation I do it like this. With formik I've created my onSubmit:
const response = await mutate({
variables: {
email: values.email,
password: values.password,
},
})
const token = response.data.login.jwt
localStorage.setItem('token', token)
history.push('/') // Navigating to the home page
And this is what I want back with the login mutation (just the token):
export const loginMutation = gql`
mutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
jwt
}
}
`
To get the currentUser's data I've put my currentUser query in my root router file. Please apologize me for naming the component PrivateRoute. I haven't renamed it yet because I can't find a proper name for it. I'm sorry. So in /src/router/index.js I have this:
// First the actual query
const meQuery = gql`
{
currentUser {
id
username
email
}
}
`
...
// A component that passess the currentUser as a prop, otherwise it will be null
const PRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
return (
<Component
{...props}
currentUser={
rest.meQuery.currentUser ? rest.meQuery.currentUser : null
}
/>
)
}}
/>
)
}
// Another component that uses the meQuery so I later can access it if I use the PrivateRoute component.
const PrivateRoute = graphql(meQuery, { name: 'meQuery' })(PRoute)
// Using the PrivateRoute. And in my Home component I can later grap the currentUser via propsb
const App = () => (
<Router>
<div>
<PrivateRoute exact path="/" component={Home} />
...
In the Home component I grab the prop:
const { data: { loading, error, getAllPosts = [], currentUser } } = this.props
I pass it down to my Navbar component:
<Navbar currentUser={this.props.currentUser} />
And in the Navbar component I take the username if it exists:
const { username } = this.props.currentUser || {}
And then I render it.
This is what I'm having troubles with
My application is currently trying to get the currentUser when I get to the /login route. And after I've successfully loged in I get back the token, but the currentUser query is not being fetched again. Thefore I have to refresh my page to get the current user and all of it's values.
I have also created a little video that demonstrates what my problem is. I believe it will show you more clearly the problem than me trying to type it.
Here's the video: https://www.youtube.com/watch?v=GyI_itthtaE
I also want to thank you for reading my post and that you hopefully are going to help me. I have no idea why this is happening to me and I just can't seem to solve it. I've tried to write this question as best as I can, so sorry if it was confusing to read.
Thanks
I think you might be able to solve this issue by setting the query's FetchPolicy to "cache-and-network". You can read about fetch policies here: "GraphQL query options.fetchPolicy"
in your specific case I think you can update this line
const PrivateRoute = graphql(meQuery, { name: 'meQuery' })(PRoute)
to this:
const PrivateRoute = graphql(meQuery, { name: 'meQuery', options: {fetchPolicy: 'cache-and-network'} })(PRoute)
Explanation
As stated in the documentation, the default policy is cache-first.
currentUser is queried the first time and it updates the cache.
You execute the login mutation, the cache is not updated without
you updating it (read about it here: "Updating the cache after a
mutation").
currentUser query is executed again but due to the default cache-first policy the outdated result will be retrieved only from the cache.
From the official documentation:
cache-first: This is the default value where we always try reading
data from your cache first. If all the data needed to fulfill your
query is in the cache then that data will be returned. Apollo will
only fetch from the network if a cached result is not available. This
fetch policy aims to minimize the number of network requests sent when
rendering your component.
cache-and-network: This fetch policy will have Apollo first trying to read data from your cache. If all the data needed to fulfill your
query is in the cache then that data will be returned. However,
regardless of whether or not the full data is in your cache this
fetchPolicy will always execute query with the network interface
unlike cache-first which will only execute your query if the query
data is not in your cache. This fetch policy optimizes for users
getting a quick response while also trying to keep cached data
consistent with your server data at the cost of extra network
requests.
in addition to these two there are two more policies: 'network-only' and 'cache-only' here's the link to the documentation
for me it worked when I refetched the currentUser query in my login mutation. I added my code below. Maybe it helps:
onSubmit(event) {
event.preventDefault();
const { email, password } = this.state;
this.props
.mutate({
variables: { email, password },
update: (proxy, { data }) => {
// Get Token from response and set it to the localStorage
localStorage.setItem('token', data.login.jwt);
},
refetchQueries: [{ query: currentUser }]
})
.catch((res) => {
const errors = res.graphQLErrors.map(error => error.message);
this.setState({ errors });
});
}