I have a component called PlatformMain that currently depends on a global channel object from Phoenix defined inside of the component's file.
let channel;
let socket = new Socket("...", {params: {token: window.userToken}});
socket.connect();
class PlatformMain extends React.Component {
componentWillMount() {
this.connectUser();
}
connectUser() {
const { user } = this.props;
channel = socket.channel("user_pool:" + user.email, { app: APP });
this.setupChannel();
}
setupChannel() {
channel.join()
.receive("ok", () => { console.log("Successfully joined call channel") })
.receive("error", () => { console.log("Unable to join") })
channel.on("match_found", payload => {
...
});
...
}
If the user presses a button, I'd like that to dispatch an action as well as push a message to the channel.
onPress() {
console.log("APPROVE_MATCH");
const { peer, waitForResponse } = this.props;
approveMatch(peer);
channel.push("approve_match", { // <------ want to put this somewhere else
"matched_client_email": peer.email,
});
}
My question is, if I want to "reduxify" the channel.push call, where should I put it? It feels weird not having the channel.push(...) somewhere else since it's an API call. I was going to put it in a saga using redux-saga like so:
function* approveMatch(action) {
const peer = action.payload.peer;
channel.push("approve_match", { // <------- but how would I get the same channel object?
"matched_client_email": peer.email,
});
}
export default function* watchMatchingStatus() {
yield takeEvery(matchingStatusActions.APPROVE_MATCH, approveMatch);
}
But wouldn't I need to point to the same channel object? How would I do that? If I put the initialization of the channel in its own file and I export and import it in multiple places, wouldn't it execute the file multiple times (and consequently join the channel multiple times)?
You could put the initialization of channel in its own file and safely import multiple times, the evaluation of the module only happens once. You can check the spec for confirmation:
Do nothing if this module has already been evaluated. Otherwise, transitively evaluate all module dependences of this module and then evaluate this module.
Related
The problem:
Nothing happends when throwing throw redirect(302, '/auth/sign-up-success') in SvelteKit's actions if onSuccess: () => {...} is set in Felte's createForm({...}).
Example:
// +page.server.ts
export const actions: Actions = {
default: async (event) => {
...
throw redirect(302, '/auth/sign-up-success');
}
}
// SignUpForm.svelte
const { form, errors } = createForm({
onSuccess: (response) => {
invalidate('app:auth')
},
...
}
If I would delete the onSuccess part, then redirect would happend.
Question:
Is there a way to reuse that redirect form success response logic from default Felte form config without writing it again myself?
Action responses are JSON objects with a type, you could read the response and redirect on the client:
async onSuccess(response) {
const { type, location } = await response.json();
if (type == 'redirect') {
goto(location); // from '$app/navigation'
return;
}
}
I would not recommend using this library though. It appears to be incompatible with SSR and one of its main actions shares the name of the form data property used by SvelteKit form actions.
Depending on why you are using this, there might be more suitable tools for SvelteKit in particular (if you even need any, SvelteKit does many things out of the box).
So at the moment I am having to put my request / api logic directly into my components because what I need to do a lot of the time is set state based on the response I get from the back end.
Below is a function that I have on my settings page that I use to save the settings to recoil after the user hits save on the form:
const setUserConfig = useSetRecoilState(userAtoms.userConfig);
const submitSettings = async (values: UserConfigInterface) => {
try {
const { data: {data} } = await updateUser(values);
setUserConfig({
...data
});
} catch (error) {
console.log('settings form error: ', error);
}
}
This works perfectly...I just dont want the function in my component as most of my components are getting way bigger than they need to be.
I have tried making a separate file to do this but I can only use the recoil hooks (in this instance useSetRecoilState) inside of components and it just complains when I try and do this outside of a react component.
I have tried implementing this with recoils selector and selectorFamily functions but it gets kind of complicated. Here is how I have tried it inside of a file that has atoms / selectors only:
export const languageProgress = atom<LanguageProgress>({
key: "LanguageProgress",
default: {
level: 1,
xp: 0,
max_xp: 0
}
})
export const languageProgressUpdate = selectorFamily<LanguageProgress>({
key: "LanguageProgress",
get: () => async () => {
try {
const { data: { data } } = await getLanguageProgress();
return data;
} catch (error) {
console.log('get language progress error');
}
},
set: (params:object) => async ({set}) => {
try {
const { data: { data } } = await updateLanguageProgress(params);
set(languageProgress, {
level: data.level,
xp: data.xp,
max_xp: data.max_xp
});
} catch (error) {
console.log('language progress update error: ', error);
}
}
});
What I want to do here is get the values I need from the back end and display it in the front which I can do in the selector function get but now I have 2 points of truth for this...my languageProgress atom will initially be incorrect as its not getting anything from the database so I have to use useGetRevoilValue on the languageProgressUpdate selector I have made but then when I want to update I am updating the atom and not the actual value.
I cannot find a good example anywhere that does what I am trying to here (very suprisingly as I would have thought it is quite a common way to do things...get data from back end and set it in state.) and I can't figure out a way to do it without doing it in the component (as in the first example). Ideally I would like something like the first example but outside of a component because that solution is super simple and works for me.
So I dont know if this is the best answer but it does work and ultimately what I wanted to do was seperate the logic from the screen component.
The answer in my situation is a bit long winded but this is what I used to solve the problem: https://medium.com/geekculture/crud-with-recoiljs-and-remote-api-e36581b77168
Essentially the answer is to put all the logic into a hook and get state from the api and set it there.
get data from back end and set it in state
You may be looking for useRecoilValueLoadable:
"This hook is intended to be used for reading the value of asynchronous selectors. This hook will subscribe the component to the given state."
Here's a quick demonstration of how I've previously used it. To quickly summarise: you pass useRecoilValueLoadable a selector (that you've defined somewhere outside the logic of the component), that selector grabs the data from your API, and that all gets fed back via useRecoilValueLoadable as an array of 1) the current state of the value returned, and 2) the content of that API call.
Note: in this example I'm passing an array of values to the selector each of which makes a separate API call.
App.js
const { state, contents } = useRecoilValueLoadable(myQuery(arr));
if (state.hasValue && contents.length) {
// `map` over the contents
}
selector.js
import { selectorFamily } from 'recoil';
export const myQuery = selectorFamily({
key: 'myQuery',
get: arr => async () => {
const promises = arr.map(async item => {
try {
const response = await fetch(`/endpoint/${item.id}`);
if (response.ok) return response.json();
throw Error('API request not fulfilled');
} catch (err) {
console.log(err);
}
});
const items = await Promise.all(promises);
return items;
}
});
I am working on a react app with redux. I did implement thunk actions to:
Create a web3modal instance
Register a ethersjs provider
Register a ethersjs signer
Everything is very basic and simple.
However whenever I try to make use of the on events (doesn't matter if provider oder contract), they simply won't fire.
I have 2 files:
walletSlice.ts which will handle all the redux action and reducer logic with #reduxjs/toolkit .
wallet-api.ts which has all the relevant functions to interact with the wallet.
The walletSlice.ts relevant part looks exactly like this:
export const connectWallet = createAsyncThunk(
'web3wallet/connectWallet',
async (arg, thunkApi) => {
const instance = await WalletAPI.registerWalletInstance();
provider = await WalletAPI.registerWalletProvider(instance);
signer = await WalletAPI.registerWalletSigner(provider);
return Promise.resolve();
}
);
The wallet-api.ts relevant parts look exactly like this:
import { ethers, providers } from 'ethers';
import Web3Modal from 'web3modal';
// get Web3Modal Instance
export async function registerWalletInstance(): Promise<Web3Modal> {
const providerOptions = {};
const web3Modal = new Web3Modal({
providerOptions,
});
const instance = await web3Modal.connect();
return Promise.resolve(instance);
}
/**
* register Wallet provider.
* Events on provider #see https://docs.ethers.io/v5/api/providers/provider/#Provider--event-methods
* Implementing the EIP-1193 Standard #see https://eips.ethereum.org/EIPS/eip-1193
*/
export async function registerWalletProvider(
instance: any
): Promise<providers.JsonRpcProvider> {
const provider = new ethers.providers.Web3Provider(instance);
// Subscribe to accounts change
provider.on('accountsChanged', (accounts: string[]) => {
console.log(accounts);
});
// Subscribe to chainId change
provider.on('chainChanged', (chainId: number) => {
console.log(chainId);
});
// Subscribe to provider connection
provider.on('connect', (info: { chainId: number }) => {
console.log(info);
});
// Subscribe to provider disconnection
provider.on('disconnect', (error: { code: number; message: string }) => {
console.log(error);
});
provider.on('error', (tx) => {
// Emitted when any error occurs
console.log({ tx });
});
return Promise.resolve(provider);
}
// register Wallet signer.
export async function registerWalletSigner(
provider: providers.JsonRpcProvider
): Promise<providers.JsonRpcSigner> {
const signer = provider.getSigner();
return Promise.resolve(signer);
}
None of the provider.on() events will fire. I've tried to change networks from rinkeby to polygon or mainnet, but nothing happens. When I disconnect from the site, nothing happens. It is the same with all provider events as shown in wallet-api.ts. I did try the same approach with another file called contract-api.ts. However the contract events won't fire either.
I tried to use the provider.on() events with useEffect() or useCallback(). Moved the code to a standalone tsx. But nothing happened.
await web3Modal.connect() already returns a provider;
try to use that one instead of new ethers.providers.Web3Provider(instance);
I'm new to React and Ionic as well as JS in general so I've followed the tutorial and tried to adapt it to my use case.
What I want to achieve is the following:
read a JSON string from local storage when the app loads
write the (newer) JSON back to storage when the app quits
What I have right now is (heavily truncated):
ExploreContainer.tsx
import { useStore } from '../hooks/useFilesystem';
var vars = { intervaltime : 50000 /* ... */ };
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
const { writeVars, readVars } = useStore();
writeVars();
if ( vars.intervaltime == 50000 ) {
vars = JSON.parse( readVars() );
}
console.log( vars );
// ...
useFilesystem.ts
import { useStorage } from '#ionic/react-hooks/storage';
import { vars } from '../components/ExploreContainer';
const FILESTORAGE = "files";
export function useStore() {
const { get, set } = useStorage();
const writeVars = async () => {
set(FILESTORAGE, JSON.stringify(vars));
};
const readVars = function() {
get(FILESTORAGE).then((value) => { return value });
}
return {
writeVars,
readVars
};
};
The problem right now:
The readVars() call in the React.FC doesn't wait for the get in the custom hook. When I log the output on read, I see that it's an unfulfilled promise. Naturally, this prompts the JSON.parse() to throw an error, because the string it could parse isn't read yet.
I tried to declare an async function in the React.FC so I could await it, but function declarations are not possible because the FC is in strict mode always.
I also tried to declare the async function before the React.FC but this doesn't work as well, because I mustn't call the hook outside the FC if I interpreted the error messages correctly.
In the custom hook I previously had an await statement instead of the .then, but the behavior was the same.
If there is an easier way to read and write a JSON string into some form of persistent(!) storage, I'd be happy to learn about it!
I see this as classic async/await issue only.
As you declared function 'writeVars' as async, so I expect function 'set()' is async in nature. If so please add await in front of 'set' function.
If 'get' function also an async one, then add await in front of it and add 'async' to 'readVars' function declaration.
If point 1 and point 2 are correct, then add await in front of functions 'writeVars' and 'readVars' where ever you call them and add async to the function declaration of calling function.
The problem was not being able to await in the React.FC-block.
However, the following worked like a charm:
var readPromise = readVars();
readPromise.then(function(result) {
vars = JSON.parse(result);
//doing stuff to the data
});
This ensured that the JSON.parse(result) only executes when the readVars-promise has been successfully resolved.
Currently, when socket.io emits a 'gmessage' from my server, and the socket in my component catches it, my entire state is replaced.
So the current flow is like this:
I emit a message from the component, it goes to my server, then to the watson API. The API sends a response to my server, and the server sends that to the component.
So that first message creates the connection to socket.io, then the second socket.io event is caught on the same connection.
Is there a better way to set up the connection to socket.io and handle both the emit and the "on gmessage" parts of this? Thank you for any tips in advance. I'm still new to react, so anything you see that I should do differently is helpful!!
...
import {Launcher} from 'react-chat-window'
import io from 'socket.io-client';
import { getUser } from '../actions/userActions';
class Workshop extends Component {
constructor() {
super();
this.state = {
messageList: []
};
}
_onMessageWasSent(message) {
this.setState({
messageList: [message, ...this.state.messageList]
})
var socket = io.connect('http://localhost:5000');
socket.emit('message', { message });
var myThis = this
var myState = this.state
socket.on('gmesssage', function (data) {
console.log(data);
myThis.setState({
messageList: [{
author: 'them',
type: 'text',
data: data
}, ...myState.messageList]
})
})
}
render() {
return (
<Grid container className="DashboardPage" justify="center">
<Grid item xs={12}>
<div>Welcome to your Workshop</div>
<TeamsTaskLists />
</Grid>
<Launcher
agentProfile={{
teamName: 'Geppetto',
imageUrl: 'https://geppetto.ai/wp-content/uploads/2019/03/geppetto-chat-avi.png'
}}
onMessageWasSent={this._onMessageWasSent.bind(this)}
messageList={this.state.messageList}
showEmoji
/>
</Grid>
);
}
}
const mapStatetoProps = state => ({
user: state.user
});
export default connect(
mapStatetoProps,
{ getUser }
)(Workshop);
Fareed has provided a working Redux solution and suggested improvements for a non-Redux approach, so I'd like to address the issues in your current code:
You're reinitializing the socket variable and creating its listener every time a new message is received instead of configuring and initializing the socket object just once inside a separate file and then consuming it by your Workshop component and possibly other components across your project.
You're appending the newly received message by calling setState({ messages: [...] }) twice in _onMessageWasSent.
To solve the first issue, you can create a separate file and move your socket related imports and initialization to it like:
// socket-helper.js
import io from 'socket.io-client';
export const socket = io.connect('http://localhost:5000');
Bear in mind, this is a minimal configuration, you can tweak your socket object before exporting it as much as the socket.io API allows you to.
Then, inside the Workshop component's componentDidMount subscribe to this socket object like:
import { socket } from 'path/to/socket-helper.js';
class Workshop extends Component {
constructor(props) {
super(props);
...
}
componentDidMount() {
// we subscribe to the imported socket object just once
// by using arrow functions, no need to assign this to myThis
socket.on('gmesssage', (data) => {
this._onMessageWasSent(data);
})
}
...
}
React docs say
If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
componentDidMount will run only once, so your listener will not get redefined multiple times.
To solve the second issue, _onMessageWasSent handler function should only receive the new message and append it to previous messages using setState, like this:
_onMessageWasSent(data) {
this.setState(previousState => ({
messageList: [
{
author: 'them',
type: 'text',
data: data,
},
...previousState.messageList,
]
}))
}
Since you are already using redux it's better to make socket.io dispatch redux actions for you, you can use redux saga same as this article describes or you can use this implementation without saga but you need to have redux-thunk middleware :
1- Create a file lets say lib/socket.js
then inside this file create socket.io client lets call it socket and create initSocketIO function where you can fire any redux actions in response to socket.io:
import socket_io from "socket.io-client";
// our socket.io client
export const socket = socket_io("http://localhost:5000");
/**
* init socket.io redux action
* #return {Function}
*/
export const initSocketIO = () => (dispatch, getState) => {
// connect
socket.on('connect', () => dispatch(appActions.socketConnected()));
// disconnect
socket.on('disconnect', () => dispatch(appActions.socketDisconnected()));
// message
socket.on('message', message => {
dispatch(conversationActions.addOrUpdateMessage(message.conversationId, message.id, message));
socket.emit("message-read", {conversationId: message.conversationId});
});
// message
socket.on('notifications', ({totalUnread, notification}) => {
dispatch(recentActions.addOrUpdateNotification(notification));
dispatch(recentActions.setTotalUnreadNotifications(totalUnread));
});
};
2- then inside your App component call this action once your App is mounted:
import React, {Component} from "react";
import {initSocketIO} from "./lib/socket.js";
export class App extends Component {
constructor(props) {
super(props);
this.store = configureStore();
}
componentDidMount() {
// init socket io
this.store.dispatch(initSocketIO());
}
// ... whatever
}
3- now you can also fire any socket.io action from any component using this:
import React, {Component} from "react";
import {socket} from "./lib/socket.js"
MyComponent extends Components {
myMethod = () => socket.emit("message",{text: "message"});
}
or from any redux action using thunk for example:
export const sendMessageAction = (text) => dispatch => {
socket.emit("my message",{text});
dispatch({type: "NEW_MESSAGE_SENT",payload:{text}});
}
Notes:
1-This will perfectly work, but still, I prefer the redux-saga method for managing any side effects like API and socket.io.
2-In your components, you don't need to bind your component methods to this just use arrow functions for example:
myMethod = () => {
// you can use "this" here
}
render = (){
return <Launcher onMessageWasSent={this.myMethod} />
}
Depending on your application, I think Redux would be overkill for this.
I would change to a lambda expression here though to avoid having to save this references:
socket.on('gmesssage', data => {
console.log(data);
this.setState({
messageList: [{
author: 'them',
type: 'text',
data: data
}, ...this.messageList]
})
})
You might consider using the new React Hooks also e.g. turn it into a stateless component and use useState:
const Workshop = () => {
const [messageList, setMessageList] = useState([]);
...