How can I test all this logic inside setState callback parameter - reactjs

I'm trying to test the following code. I'm using jest and react testing library. This is the firs time I've used setState like this. I solved my initial which was to avoid passing in the dependency of current state but I'm not sure how can I test this. Can someone please advise.
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);

This piece of code is implementation detail. React testing library enforces UI testing. You can read this article from Kent Dodds.
In your tests you can do the same thing as the user would do (fill a form, click etc.), and then check what the user should see or not see (maybe his name, his stats etc.).
And if you get data from your backend and you would like to test only the frontend, you can mock the answer of the backend.

Related

Mapping State in React with Server Requests

I'm new to React as we are trying to migrate our app from AngularJS. One thing I'm struggling to wrap my head around is what's the best way to make and cache state mapping requests.
Basically, I would do a search, that returns a list of objects and one of the field is a status code (e.g. 100, 200, 300, etc.), some number. To display the result, I need to map that number to a string and we do that with a http request to the server, something like this:
GET /lookup/:stateId
So my problem now is:
I have a list of results but not many different states, how can I make that async call (useEffect?) to make that lookup only once for different stateId? Right now, I can get it to work, but the request is made on every single mapping. I'm putting the Axio call in a utility function to try and reuse this across multiple pages doing similar things, but is that the "React" way? In AngularJS, we use the "|" filter to map the code to text.
Once I have that mapping id => string, I want to store it in cache so next one that needs to map it no longer make the http request. Right now, I put the "cache" in the application level context and use dispatch to update/add values to the cache. Is that more efficient? It appears if I do a language change, where I keep the language in the same application context state, the cache would be re-initialized, and I'm not sure what other things would reset that. In AngularJS, we used the $rootState to 'cache'.
Thanks for any pointers!
In a lookupUtil.js
const DoLookupEntry = async (entryId) => {
const lookupUrl = `/lookup/${entryId}`;
try {
const response = await Axios.get(looupUrl,);
return response.data;
} catch (expt) {
console.log('error [DoLookupEntry]:',expt);
}
}
In a formatUtils.js
const formatLookupValue = (entryId) => {
const appState = useContext(AppContext);
const appDispatch = useContext(DispatchContext);
const language = appState.language;
if (appState.lookupCache
&& appState.lookupCache[entryId]
&& appState.lookupCache[entryId][language]) {
// return cached value
const entry = appState.lookupCache[entryId][language];
return entry.translatedValue;
}
// DoLookup is async, but we are not, so we want to wait...
DoLookupEntry(entryId)
.then((entry) => { // try to save to cache when value returns
appDispatch({type: States.APP_UPDATE_LOOKUP_CACHE,
value:{language, entry}})
return entry.translatedValue;
});
}
And finally the results.js displaying the result along the line (trying formatLookupValue to map the id):
{searchState.pageResults.map((item) => {
return (
<tr>
<td><Link to={/getItem/item.id}>{item.title}</Link></td>
<td>{item.detail}</td>
<td>{formatLookupValue(item.stateId)}</td>
</tr>
)
})}

Axios Response Data not saved in State

I have a custom hook useServerStatus that fetches from a RESTful API with axios. Checking the network tab, the response went through fine, I can see all my data. Using console.log to print out the result or using debugger to check the result in the browser works flawlessly. However, calling the setState method that I get from useState will not save the response data.
ServerStatus Interface (ServerStatus.ts)
interface ServerStatus {
taskid: string
taskmodule: string
taskident?: string
status: string
server: string
customer: string
}
useServerStatus Hook (useServerStatus.ts)
export default function useServerStatus() {
const [serverStatus, setServerStatus] = useState<ServerStatus[][]>([]);
useEffect(() => {
fetchServerStatus();
}, []);
const fetchServerStatus = () => {
axios.get<ServerStatus[][]>(`${config.apiURL}/servers`)
.then(res => setServerStatus(res.data));
}
return serverStatus;
}
Network Tab
https://i.imgur.com/cWBSPVz.png
The first request you see in the network tab is handled the same exact way, no problems there.
React Developer Console
https://i.imgur.com/YCq3CPo.png
Try
const fetchServerStatus = () => {
axios.get<ServerStatus[][]>(`${config.apiURL}/servers`)
.then(res => { setServerStatus(res.data) });
}
So, to answer my own question:
I figured out that the problem wasn't about data not being saved in state, but data not correctly being received by axios.
I fixed it with a workaround. Instead of returning a ServerStatus[][] in my backend, I returned a ServerStatus[]. I was able to use this data instead.
Following the lesson here https://reactjs.org/docs/hooks-custom.html the most obvious thing that jumps out to me is that you aren't returning the state variable serverStatus in your code vs. the example is returning "isOnline". Try to match this by returning serverStatua in your custom effect to see if it helps.

Get Stream IO React Native UnFollowing option not available

I am using Get Stream Io react native in my project https://github.com/GetStream/react-native-activity-feed
I see there is a functionality in the library of following a particular user, whereas the opposite is not available i.e. how to unfollow a user.
Please point me in the right direction on how to achieve this in react native
Our library doesn't include the logic for following/unfollowing. It has a button that allows you to set it up yourself though. Which is the FollowButton you're talking about. You could do something like this:
<FollowButton
followed={async () => {
// check if you're following the user
const following = await your_timeline_feed.following({filter: ['user:user_42'] })
if (following) {
return true;
}
return false;
}} // renders the button as "following"
clicked={async () => {
// your logic for following/unfollowing
await your_timeline_feed.follow('user', 'user_42');
}}
/>
Read more here:
https://getstream.github.io/react-native-activity-feed/#!/FollowButton
https://getstream.io/docs/following/?language=js

Jest mock an import from a utility file

I am trying to standardise the way that regularly mocked functions get mocked.
So I have a function which handles a call to a home screen context in useHomeScreenContext.ts.
export const useHomeScreenContext = () => useContext(/* context here */)
Then the component I am testing uses that hook
Component.tsx
export const Component = () => {
const context = useHomeScreenContext();
}
Then my test file for Component looks like
Component.test.tsx
const contextSpy = jest.spyOn(context, 'useHomeScreenContext');
it("works", () => {
contextSpy.mockReturnValue(fakeValue)
expect(fakeValue).toBeTruthy()
})
The issue that I have with this approach is that there will be a large number of files that will need to be tested by mocking this context and the actual mock is a little bit more complex than I have put in here. So what I would like to do is to standardise the mock using a utility function, so I have created a separate file called mockHomeScreenContext.ts which looks a little something like this.
const contextSpy = jest.spyOn(context, 'useHomeScreenContext');
export const mockHomeScreenContext = (context) => {
beforeAll(() => {
contextSpy.mockReturnValue(mergeDeepLeft(context, homeScreenContextDefaults));
});
afterAll(() => {
contextSpy.mockRestore();
});
};
With the idea being that it is used inside of a describe block and it will tidy itself up at the end of the describe block like so
describe('and does not have funds', () => {
mockHomeScreenContext(contextOverrides);
it("works", () => {
// Tests here
})
});
And this seems to work really well, until I have 2 describe blocks and then things start to go a bit haywire and some tests seem to mock the data correctly but then all following tests will fail as the mock function is not returning anything. Getting rid of the afterAll call to clean up the mock helps but it then infects other tests.
I have been round and round in circles for days trying to get this working and I feel like it really shouldnt be this difficult and I am just missing a little bit of an understanding as to how jest mocks work.
I think the problem is in the fact that your contextSpy is global for all tests, because it's created outside of mockHomeScreenContext function. As you can find in documentation, mockRestore would restore the original (non-mocked) implementation. You might try to use mockClear instead, as it's a softer version (I usually just use that).
However, I would propose to not mock your context like this, but rather create a mock provider that would cause context to work properly, without a need to do extensive mocking (which as you probably see can be quie painful).
Here's example of what I have in mind:
Usage in test:
<MockProvider context={contextOverrides}>
<YourComponent/>
</MockProvider>
And implementation:
const MockProvider = ({context, children}) => {
return (
<YourContext.Provider value={mergeDeepLeft(context, homeScreenContextDefaults)}>
{children}
</YourContext.Provider>
)
}
This way you would get rid of all those hard mocks and would use real context (with some mocked value).

Caching in React

In my react App I have a input element. The search query should be memoized, which means that if the user has previously searched for 'John' and the API has provided me valid results for that query, then next time when the user types 'Joh', there should be suggestion for the user with the previously memoized values(in this case 'John' would be suggested).
I am new to react and am trying caching for the first time.I read a few articles but couldn't implement the desired functionality.
You don't clarify which API you're using nor which stack; the solution would vary somewhat depending on if you are using XHR requests or something over GraphQL.
For an asynchronous XHR request to some backend API, I would do something like the example below.
Query the API for the search term
_queryUserXHR = (searchTxt) => {
jQuery.ajax({
type: "GET",
url: url,
data: searchTxt,
success: (data) => {
this.setState({previousQueries: this.state.previousQueries.concat([searchTxt])
}
});
}
You would run this function whenever you want to do the check against your API. If the API can find the search string you query, then insert that data into a local state array variable (previousQueries in my example).
You can either return the data to be inserted from the database if there are unknowns to your view (e.g database id). Above I just insert the searchTxt which is what we send in to the function based on what the user typed in the input-field. The choice is yours here.
Get suggestions for previously searched terms
I would start by adding an input field that runs a function on the onKeyPress event:
<input type="text" onKeyPress={this._getSuggestions} />
then the function would be something like:
_getSuggestions = (e) => {
let inputValue = e.target.value;
let {previousQueries} = this.state;
let results = [];
previousQueries.forEach((q) => {
if (q.toString().indexOf(inputValue)>-1) {
result.push(a);
}
}
this.setState({suggestions: results});
}
Then you can output this.state.suggestions somewhere and add behavior there. Perhaps some keyboard navigation or something. There are many different ways to implement how the results are displayed and how you would select one.
Note: I haven't tested the code above
I guess you have somewhere a function that queries the server, such as
const queryServer = function(queryString) {
/* access the server */
}
The trick would be to memorize this core function only, so that your UI thinks its actually accessing the server.
In javascript it is very easy to implement your own memorization decorator, but you could use existing ones. For example, lru-memoize looks popular on npm. You use it this way:
const memoize = require('lru-memoize')
const queryServer_memoized = memoize(100)(queryServer)
This code keeps in memory the last 100 request results. Next, in your code, you call queryServer_memoized instead of queryServer.
You can create a memoization function:
const memo = (callback) => {
// We will save the key-value pairs in the following variable. It will be our cache storage
const cache = new Map();
return (...args) => {
// The key will be used to identify the different arguments combination. Same arguments means same key
const key = JSON.stringify(args);
// If the cache storage has the key we are looking for, return the previously stored value
if (cache.has(key)) return cache.get(key);
// If the key is new, call the function (in this case fetch)
const value = callback(...args);
// And save the new key-value pair to the cache
cache.set(key, value);
return value;
};
};
const memoizedFetch = memo(fetch);
This memo function will act like a key-value cache. If the params (in our case the URL) of the function (fetch) are the same, the function will not be executed. Instead, the previous result will be returned.
So you can just use this memoized version memoizedFetch in your useEffect to make sure network request are not repeated for that particular petition.
For example you can do:
// Place this outside your react element
const memoizedFetchJson = memo((...args) => fetch(...args).then(res => res.json()));
useEffect(() => {
memoizedFetchJson(`https://pokeapi.co/api/v2/pokemon/${pokemon}/`)
.then(response => {
setPokemonData(response);
})
.catch(error => {
console.error(error);
});
}, [pokemon]);
Demo integrated in React

Resources