Why this assign does not trigger? - javascript

After reading the official xstate tutorial, I tried to implement my own machine inspired by this post on dev.to by one of the xstate's dev.
Everything works as expected besides that output does not seem to be updated. The assignment does not do its job I think. What did I forget?
To compare, here is a working demo from xstate where the variable in the context is updated as expected.
more information on assign on Context | XState Docs
my code:
import "./styles.css";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createMachine, assign } from "xstate";
import { useMachine } from "#xstate/react";
interface FetchContext {
output: string;
}
const fetchifetch = async () => {
return await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
).then((response) => response.json());
};
const fetchMachine = createMachine<FetchContext>({
initial: "idle",
context: {
output: "wesh" // none selected
},
states: {
idle: {
on: {
FETCH: "loading"
}
},
loading: {
invoke: {
src: (context, event) => async (send) => {
setTimeout(async () => {
const data = await fetchifetch();
console.log("done");
console.log(data);
// well. here I want to assign something to output
assign({
output: (context, event) => data.title
});
send("FETCHED_SUCCESSFULLY");
}, 4000);
console.log("start");
},
onError: {
target: "idle"
}
},
on: {
FETCHED_SUCCESSFULLY: {
target: "idle"
}
}
},
fetch: {
on: {
CLOSE: "idle"
}
}
}
});
function App() {
const [current, send] = useMachine(fetchMachine);
const { output } = current.context;
return (
<div className="App">
<h1>XState React Template</h1>
<br />
<input
disabled={current.matches("loading")}
defaultValue="yo"
onChange={(e) => console.log(e.currentTarget.value)}
/>
<button
disabled={current.matches("loading")}
onClick={() => send("FETCH")}
>
click to fetch
</button>
<!-- let's display the result over here -->
<div>{output}</div>
<div>{current.context.output}</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

You need to return a Promise then and have the state machine update the context after the Promise is resolved.
The context is updated in the onDone property of invoke.
const fetchMachine = createMachine<FetchContext>({
initial: "idle",
context: {
output: "wesh" // none selected
},
states: {
idle: {
on: {
FETCH: "loading"
}
},
loading: {
invoke: {
src: (context, event) => async (send) => {
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const data = await fetchifetch();
resolve(data.title);
} catch (err) {
reject(err);
}
}, 4000);
});
},
onDone: {
target: "fetch",
actions: assign({
output: (_, event) => event.data,
})
},
onError: {
target: "idle"
}
},
on: {
FETCHED_SUCCESSFULLY: {
target: "idle",
}
}
},
fetch: {
on: {
CLOSE: "idle"
}
}
}
});

Related

react - mockImplementation not working using jest

App.tsx
import { StackNavigationProp } from '#react-navigation/stack';
...
export const App = () => {
const rootNavigation = useNavigation<StackNavigationProp<ParamList>>();
const { params } =
useRoute<RouteProp<ParamList, 'myScreen'>>();
const navigateToHomePage = (): void => {
if (params.isVerified) {
rootNavigation.reset({
index: 0,
routes: [
{
name: 'HomePage',
},
],
});
}
};
return (...)
}
App.test.tsx
import React from 'react';
import { render, fireEvent, waitFor, RenderAPI } from '#testing-library/react-native';
import { MigrationFlowContext } from 'src/contexts/migration/MigrationFlowContext';
import App from './App.tsx';
const mockedNavigate = jest.fn();
const mockedReset = jest.fn();
const mockSpy = jest.fn().mockImplementation(() => ({
params: {
isVerified: true,
},
}));
jest.mock('#react-navigation/native', () => {
const actualNav = jest.requireActual('#react-navigation/native');
return {
...actualNav,
useNavigation: () => ({
navigate: mockedNavigate,
reset: mockedReset,
}),
useRoute: async () => mockSpy(),
};
});
describe('App', () => {
beforeEach(() => {
mockSpy.mockReset();
});
describe('isVerified = true', () => {...})
describe('isVerified = false', () => {
mockSpy.mockImplementation(() => {
return {
params: {
isVerified: false,
},
};
});
test('Click Yes', async () => {
const { getByTestId } = renderHomeScreen();
...
});
test('Click No', () => {
const { getByTestId } = renderHomeScreen();
...
});
});
}
It didn't work with below error
TypeError: Cannot read properties of undefined (reading 'isVerified')
How to fix it?

graphql-ws query subscribeToMore updateQuery not firing

Im tryin to get my queries to update using the subscribeToMore function. Im getting lost on why the code is a failing to execute. so far all operations query, mutation and subscribe will work independently.
FRONTEND
export const QUERY_MESSAGES = gql`
query Messages($convoId: ID!) {
messages(convoId: $convoId) {
text
senderId
convoId
createdAt
}
}
`;
export const SUBSCRIBE_MESSAGES = gql`
subscription Messages($convoId: ID!) {
messages(convoID: $convoID) {
text
convoId
}
}
`;
const ActiveChat = ({ currentConvo, me }: any) => {
const { subscribeToMore, data } = useQuery(QUERY_MESSAGES, {
variables: {
convoId: currentConvo,
},
});
return (
<section className="chat-wrapper">
<div className="messages-container">
<Messages
messages={data.messages}
me={me}
subscribeToMessages={() => {
subscribeToMore({
document: SUBSCRIBE_MESSAGES,
variables: { convoId: currentConvo },
updateQuery: (prev, { subscriptionData }) => {
console.log("hit");
if (!subscriptionData.data) return prev;
const newFeedItem = subscriptionData.data.messages;
return Object.assign({}, prev, {
messages: [newFeedItem, ...prev.messages],
});
},
});
}}
/>
<Input currentConvo={currentConvo} />
</div>
</section>
);
};
const Messages = ({ messages, me, subscribeToMessages }: any) => {
useEffect(() => {
subscribeToMessages();
console.log("use effect ran");
});
const formatTime = (time: number) =>
new Date(time * 1000).toLocaleTimeString();
console.log(messages);
return (
messages && (
<div>
{messages.map((message: any, i: number) => {
return message.senderId === me?._id ? (
<SenderBubble
key={i}
text={message.text}
time={message.createdAt}
formatTime={formatTime}
/>
) : (
<OtherUserBubble
key={i}
text={message.text}
time={message.createdAt}
formatTime={formatTime}
/>
);
})}
</div>
)
);
};
BACKEND
const pubsub = new PubSub();
Query: {
messages: async (parent, { convoId }, context) => {
if (context.user) {
return Message.find({ convoId }).sort({ createdAt: "asc" });
}
},
},
Mutation: {
/// creates a new message using sender id and emits MESSAGE_SENT event
sendMessage: async (parent, { text, convoId }, context) => {
pubsub.publish("MESSAGE_SENT", {
message: { text, convoId },
});
return Message.create({ text, convoId, senderId: context.user._id });
},
},
Subscription: {
userActive: {
subscribe: () => pubsub.asyncIterator("ACTIVE_USERS"),
},
message: {
subscribe: withFilter(
() => pubsub.asyncIterator("MESSAGE_SENT"),
(payload, variables) => {
return payload.message.convoId === variables.convoId;
}
),
},
},
tdlr: I think subscribeToMore.updateQuery isn't firing but i don't how to debug
SOLVED: turns out I placed the wrong url and port number while building the client so the ws wasn't connecting.

After uploading image in draft js i want to preview in the react

The problem is I want to preview the image and other elements after the user has edited in draft js but I got failed.
here what I have tried
import { EditorState } from "draft-js";
import { Editor } from "react-draft-wysiwyg";
import { convertToHTML } from "draft-convert";
import DOMPurify from "dompurify";
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
import axios from "axios";
import value from "../../redux/constant";
const RichTextEditor2 = () => {
const [editorState, setEditorState] = useState(() =>
EditorState.createEmpty()
);
const [convertedContent, setConvertedContent] = useState(null);
const handleEditorChange = (state) => {
setEditorState(state);
convertContentToHTML();
};
const convertContentToHTML = () => {
let currentContentAsHTML = convertToHTML(editorState.getCurrentContent());
setConvertedContent(currentContentAsHTML);
};
const createMarkup = (html) => {
return {
__html: DOMPurify.sanitize(html),
};
};
function uploadImageCallBack(file) {
return new Promise((resolve, reject) => {
let iFormData = new FormData();
iFormData.append("image", file);
let url = `${value.BASE_URL}uploadfile`;
axios
.post(url, iFormData, {
headers: {
"content-type": "multipart/form-data",
},
})
.then(
(response) => {
var link = `${value.BASE_URL}subjects/${response.data.data.filename}`;
resolve({ data: { link: link } });
},
(err) => {
console.log({ err: err.message });
reject(err.message);
}
);
});
}
return (
<div className="App">
<header className="App-header">Rich Text Editor Example</header>
<Editor
editorState={editorState}
onEditorStateChange={handleEditorChange}
wrapperClassName="wrapper-class"
editorClassName="editor-class"
toolbarClassName="toolbar-class"
toolbar={{
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true },
image: {
uploadCallback: uploadImageCallBack,
alt: { present: true, mandatory: true },
},
}}
/>
<div
className="preview"
dangerouslySetInnerHTML={createMarkup(convertedContent)}
></div>
</div>
);
};
export default RichTextEditor2;
when I preview all the elements in preview class everything gets previewed but only images are not showing. even the image is showing in the editor, not in the preview where I did wrong or what went wrong here can anyone please explain to me.

Apollo MockedProvider not returning expected data

I wrote a hook that calls apollo useQuery. It's pretty simple:
useDecider:
import { useState } from 'react';
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = name => {
const [enabled, setEnabled] = useState(false);
useQuery(GET_DECIDER, {
variables: {
name
},
onCompleted: data => {
const decision = data?.deciders[0]?.decision;
setEnabled(decision);
},
onError: error => {
return error;
}
});
return {
enabled
};
};
I'm trying to test it now and the MockedProvider is not returning the expected data:
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
const getMock = (value = false, decider = '') => [
{
request: {
query: GET_DECIDER,
variables: {
name: decider
}
},
result: () => {
console.log('APOLLO RESULT');
return {
data: {
deciders: [
{
decision: value,
name: decider,
value: 10
}
]
}
};
}
}
];
const FakeComponent = ({ decider }) => {
const { enabled } = useDecider(decider);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
const WrappedComponent = ({ decider, value }) => (
<MockedProvider mocks={getMock(value, decider)} addTypename={false}>
<FakeComponent decider={decider} />
</MockedProvider>
);
describe('useDecider', () => {
it('when decider returns true', () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = screen.getByText('isEnabled');
expect(result).toBeInTheDocument();
});
});
I simplified your hook implementation and put together a working example:
import { useQuery, gql } from "#apollo/client";
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { data } = useQuery(GET_DECIDER, { variables: { name } });
return { enabled: data?.deciders[0]?.decision || false };
};
Note that in the test I also updated your getBy to an await findBy:
describe("useDecider", () => {
it("when decider returns true", async () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = await screen.findByText("isEnabled");
expect(result).toBeInTheDocument();
});
});
This is because you need to wait for your API call to complete before the data will be on the page, hence you would not expect the data to be there on the first render.
From https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
To test how your component is rendered after its query completes, you
can await a zero-millisecond timeout before performing your checks.
This delays the checks until the next "tick" of the event loop, which
gives MockedProvider an opportunity to populate the mocked result
try adding before your expect call
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

The right way to draw a Map when data is ready

I need to render a map using Mapbox only when data is ready.
I have the following code in my Vuex store:
/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
In my App.vue I fetch all APIs requests using an async created method.
App.vue:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
async created() {
await this.$store.dispatch("FETCH_ACTIVITY");
await this.$store.dispatch("FETCH_GEO_IPS");
},
};
</script>
In my Dashboard component I have a conditional rendering to draw the maps component only when geoIps.length > 0
Dashboard.vue:
<template>
<div v-if="geoIps.length > 0">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
geoIps() {
return this.$store.state.geoIps;
},
};
</script>
Then I load the Maps component.
<template>
<q-card class="bg-primary APP__card APP__card-highlight">
<q-card-section class="no-padding no-margin">
<div id="map"></div>
</q-card-section>
</q-card>
</template>
<script>
import "mapbox-gl/dist/mapbox-gl.css";
import mapboxgl from "mapbox-gl/dist/mapbox-gl";
export default {
name: "maps-geo-ips-card",
computed: {
geoIps() {
return this.$store.state.geoIps;
},
},
created() {
mapboxgl.accessToken = process.env.MAPBOX_API_KEY;
},
mounted() {
const mapbox = new mapboxgl.Map({
container: "map",
center: [0, 15],
zoom: 1,
});
this.geoIps.map((geoIp) =>
new mapboxgl.Marker()
.setLngLat([geoIp.longitude, geoIp.latitude])
.addTo(mapbox)
);
},
};
</script>
<style>
#map {
height: 500px;
width: 100%;
border-radius: 25px;
overflow: hidden;
}
</style>
The problem is that when the function resolves the first IP address, the map is drawn showing only one address and not all the others like this:
What is the best way to only draw the map when my FETCH_GEO_IPS function has finished?
Thanks in advance
I think the answer lies in this bit of code:
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
Your map function loops through every element of the array and commits each IP one by one. So when the first one is committed, your v-if="geoIps.length > 0" is true.
A workaround would be to set a flag only when the IPs are set.
This is a proposed solution:
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
isReady: false
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
setIsReady: (state, value) => {
state.isReady = value;
}
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
let tofetch = getters.activityIps.length; // get the number of fetch to do
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
toFetch -= 1; // decrement after each commit
if (toFetch === 0) {
commit("setIsReady", true); // all commits are done
}
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
And in your view:
<template>
<div v-if="isReady">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
isReady() {
return this.$store.state.isReady;
},
};
</script>

Categories