suppose we try to connect web socket. web socket server sends some data for fetching client status which represents the online or offline. I tried to store these data into redux (works fine), but I need to change these statuses instantly with overriding the existing objects. I need some functionality for override my redux store. I get so far with the snippet below.
but:
this code push objects to my redux store not overriding
const [status, set_status] = useState([]);
useEffect(() => {
const socket = io(ws_api, {
query: `token=${localStorage.getItem("token")}`,
});
socket.on("message", (data) => {
status.map((item) => {
if (item.application === data.application) {
item["status"] = data.status;
} else {
set_status((status) => [...status, data]);
}
});
});
}, []);
useEffect(() => {
get_client_status(status); // redux action
}, [status]);
the data structure which is coming from the socket on message
{
application: "5ede25f4d3fde1c8a70f0a38"
client: "5ede25f4d3fde1c8a70f0a36"
status: "offline"
}
First search current state for any existing data elements, if found then update it, otherwise add to the array.
Using array::findIndex
const messageHandler = data => {
const dataIndex = status.findIndex(
item => item.application === data.application
);
if (dataIndex !== -1) {
set_status(status =>
status.map(item => {
return item.application === data.application
? { ...item, status: data.status }
: item;
})
);
} else {
set_status(status => [...status, data]);
}
});
Using array::find
const messageHandler = data => {
const found = status.find(item => item.application === data.application);
if (found) {
set_status(status =>
status.map(item => {
return item.application === data.application
? { ...item, status: data.status }
: item;
})
);
} else {
set_status(status => [...status, data]);
}
});
Edit: define this callback outside the effect
useEffect(() => {
socket.on("message", messageHandler);
}, []);
Related
I am using redux-tookit, rtk-query (for querying other api's and not just Firebase) and Firebase (for authentication and db).
The code below works just fine for retrieving and caching the data but I wish to take advantage of both rtk-query caching as well as Firebase event subscribing, so that when ever a change is made in the DB (from any source even directly in firebase console) the cache is updated.
I have tried both updateQueryCache and invalidateTags but so far I am not able to find an ideal approach that works.
Any assistance in pointing me in the right direction would be greatly appreciated.
// firebase.ts
export const onRead = (
collection: string,
callback: (snapshort: DataSnapshot) => void,
options: ListenOptions = { onlyOnce: false }
) => onValue(ref(db, collection), callback, options);
export async function getCollection<T>(
collection: string,
onlyOnce: boolean = false
): Promise<T> {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => reject('Request timed out!'), ASYNC_TIMEOUT);
onRead(collection, (snapshot) => resolve(snapshot.val()), { onlyOnce });
}).finally(() => clearTimeout(timeout));
}
// awards.ts
const awards = dbApi
.enhanceEndpoints({ addTagTypes: ['Themes'] })
.injectEndpoints({
endpoints: (builder) => ({
getThemes: builder.query<ThemeData[], void>({
async queryFn(arg, api) {
try {
const { auth } = api.getState() as RootState;
const programme = auth.user?.unit.guidingProgramme!;
const path = `/themes/${programme}`;
const themes = await getCollection<ThemeData[]>(path, true);
return { data: themes };
} catch (error) {
return { error: error as FirebaseError };
}
},
providesTags: ['Themes'],
keepUnusedDataFor: 1000 * 60
}),
getTheme: builder.query<ThemeData, string | undefined>({
async queryFn(slug, api) {
try {
const initiate = awards.endpoints.getThemes.initiate;
const getThemes = api.dispatch(initiate());
const { data } = (await getThemes) as ApiResponse<ThemeData[]>;
const name = slug
?.split('-')
.map(
(value) =>
value.substring(0, 1).toUpperCase() +
value.substring(1).toLowerCase()
)
.join(' ');
return { data: data?.find((theme) => theme.name === name) };
} catch (error) {
return { error: error as FirebaseError };
}
},
keepUnusedDataFor: 0
})
})
});
This component is for counting views at page level in Next.js app deployed on AWS Lambda
function ViewsCounter({ slug }: { slug: string }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, { method: "POST" })
.catch(console.log);
registerView();
}, [slug]);
return (
<>
{views}
</>
);
}
This one is for displaying views on homepage
function ViewsDisplay({ slug }: { slug: string }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
return (
<>
{views}
</>
);
}
While it works as expected on localhost, looks like it displays only the first fetched value and doesn't revalidate it for some reason.
When visiting the page, Counter is triggered correctly and the value is changed in DB.
Probably it has something to do with mutating, any hints are appreciated.
useSWR won't automatically refetch data by default.
You can either enable automatic refetch using the refreshInterval option.
const { data } = useSWR(`/api/views/${slug}`, fetcher, { refreshInterval: 1000 });
Or explicitly update the data yourself using a mutation after the POST request to the API.
function ViewsCounter({ slug }: { slug: string }) {
const { data, mutate } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, { method: "POST" })
.then(() => {
mutate();
})
.catch(console.log);
registerView();
}, [slug]);
return (<>{views}</>);
}
I'm learning react and I'm having difficulty putting data from Firebase into the application state. I use Rebase but I am open to any solution!
still have an error similar to this one :
Thank you for your help !
Here is the code :
class App extends Component {
state = {
user:'',
vampires: {}
}
componentDidMount() {
if(this.state.user === ''){
firebase.auth().onAuthStateChanged(user => {
if(user){
this.handleUserLogin({ user })
this.setVampires({ user })
} else {
console.log('error')
}
})
}
}
setVampires = async (authData) => {
console.log(this.state.user)
await base.fetch(`/${authData.user.uid}/vampires`, { context: this })
.then(data => {
console.log(data)
let vampires = this.state.vampires;
vampires = {..._.cloneDeep(data)};
this.setState({vampires: {vampires}})
})
}
handleUserLogin = async authData => {
this.setState({user: authData.user.uid})
}
Your Firebase data is returned as an Object, with properties VampM5-..... React expects that you pass any repeated data as an array, not as an object.
So you'll need to convert the data from Firebase to an array, for example with:
await base.fetch(`/${authData.user.uid}/vampires`, { context: this })
.then(data => {
vampires = [];
data.forEach((child) => {
vampires.push({ ...child.val(), ".key": child.key });
})
this.setState({ vampires: vampires })
})
I started integrating websockets into an existing React/Django app following along with this example (accompanying repo here). In that repo, the websocket interface is in websockets.js, and is implemented in containers/Chat.js.
I can get that code working correctly as-is.
I then started re-writing my implementation to use Hooks, and hit a little wall. The data flows through the socket correctly, arrives in the handler of each client correctly, and within the handler can read the correct state. Within that handler, I'm calling my useState function to update state with the incoming data.
Originally I had a problem of my single useState function within addMessage() inconsistently firing (1 in 10 times?). I split my one useState hook into two (one for current message, one for all messages). Now in addMessage() upon receiving data from the server, my setAllMessages hook will only update the client where I type the message in - no other clients. All clients receive/can log the data correctly, they just don't run the setAllMessages function.
If I push to an empty array outside the function, it works as expected. So it seems like a problem in the function update cycle, but I haven't been able to track it down.
Here's my version of websocket.js:
class WebSocketService {
static instance = null;
static getInstance() {
if (!WebSocketService.instance) {
WebSocketService.instance = new WebSocketService();
}
return WebSocketService.instance;
}
constructor() {
this.socketRef = null;
this.callbacks = {};
}
disconnect() {
this.socketRef.close();
}
connect(chatUrl) {
const path = `${URLS.SOCKET.BASE}${URLS.SOCKET.TEST}`;
this.socketRef = new WebSocket(path);
this.socketRef.onopen = () => {
console.log('WebSocket open');
};
this.socketRef.onmessage = e => {
this.socketNewMessage(e.data);
};
this.socketRef.onerror = e => {
console.log(e.message);
};
this.socketRef.onclose = () => {
this.connect();
};
}
socketNewMessage(data) {
const parsedData = JSON.parse(data);
const { command } = parsedData;
if (Object.keys(this.callbacks).length === 0) {
return;
}
Object.keys(SOCKET_COMMANDS).forEach(clientCommand => {
if (command === SOCKET_COMMANDS[clientCommand]) {
this.callbacks[command](parsedData.presentation);
}
});
}
backend_receive_data_then_post_new(message) {
this.sendMessage({
command_for_backend: 'backend_receive_data_then_post_new',
message: message.content,
from: message.from,
});
}
sendMessage(data) {
try {
this.socketRef.send(JSON.stringify({ ...data }));
} catch (err) {
console.log(err.message);
}
}
addCallbacks(allCallbacks) {
Object.keys(SOCKET_COMMANDS).forEach(command => {
this.callbacks[SOCKET_COMMANDS[command]] = allCallbacks;
});
}
state() {
return this.socketRef.readyState;
}
}
const WebSocketInstance = WebSocketService.getInstance();
export default WebSocketInstance;
And here's my version of Chat.js
export function Chat() {
const [allMessages, setAllMessages] = useState([]);
const [currMessage, setCurrMessage] = useState('');
function waitForSocketConnection(callback) {
setTimeout(() => {
if (WebSocketInstance.state() === 1) {
callback();
} else {
waitForSocketConnection(callback);
}
}, 100);
}
waitForSocketConnection(() => {
const allCallbacks = [addMessage];
allCallbacks.forEach(callback => {
WebSocketInstance.addCallbacks(callback);
});
});
/*
* This is the problem area
* `incoming` shows the correct data, and I have access to all state
* But `setAllMessages` only updates on the client I type the message into
*/
const addMessage = (incoming) => {
setAllMessages([incoming]);
};
// update with value from input
const messageChangeHandler = e => {
setCurrMessage(e.target.value);
};
// Send data to socket interface, then to server
const sendMessageHandler = e => {
e.preventDefault();
const messageObject = {
from: 'user',
content: currMessage,
};
setCurrMessage('');
WebSocketInstance.backend_receive_data_then_post_new(messageObject);
};
return (
<div>
// rendering stuff here
</div>
);
}
There is no need to rewrite everything into functional components with hooks.
You should decompose it functionally - main (parent, class/FC) for initialization and providing [data and] methods (as props) to 2 functional childrens/components responsible for rendering list and input (new message).
If you still need it ... useEffect is a key ... as all code is run on every render in functional components ... including function definitions, redefinitions, new refs, duplications in callbacks array etc.
You can try to move all once defined functions into useEffect
useEffect(() => {
const waitForSocketConnection = (callback) => {
...
}
const addMessage = (incoming) => {
setAllMessages([incoming]);
};
waitForSocketConnection(() => {
...
}
}, [] ); // <<< RUN ONCE
I'm using the web API for Firestore to perform a simple query ordered on a date property formatted as a string ('2017-12-30'). I use the onSnapshot() method to subscribe as a listener to document changes. The initial population of list of results works as expected - the order is correct.
As I make changes to the data, the callback then gets called with a change type of 'modified'. If any of the changes affects the date property, then I have no way of re-ordering the item in the list of results - unlike the old Realtime Database. That is, until I saw the newIndex and oldIndex properties of DocumentChange. They are undocumented for the Web API (https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentChange), but are documented as part of the Node.js API (https://cloud.google.com/nodejs/docs/reference/firestore/0.10.x/DocumentChange).
So, my problem seemed to be solved - except that in practice the values in newIndex and oldIndex seem to be largely random and bear no relation to the actual order if I refresh the query. I can't make out any pattern that would explain the index values I get back.
Has anyone used DocumentChange.newIndex and DocumentChange.oldIndex successfully? If not, how would you reorder results in subscribers as a result of changes?
const query = firestore.collection(`users/${uid}/things`).
orderBy('sortDate', 'desc').limit(1000)
query.onSnapshot(snapshot => {
snapshot.docChanges.forEach(change => {
if (change.type === "added") {
dispatch(addThing({
id: change.doc.id,
...change.doc.data()
}, change.newIndex)
}
if (change.type === "modified") {
dispatch(changeThing({
id: change.doc.id,
...change.doc.data()
}, change.oldIndex, change.newIndex))
}
if (change.type === "removed") {
dispatch(removeThing(change.doc.id, change.oldIndex))
}
})
})
The original problem I had with the DocumentChange indexes was due to a couple of bugs elsewhere in my code. As I didn't find any examples of this in use outside of the Node.js Firestore docs, here's the test code I used to verify its correct behaviour (ES6). It assumes firebase has been initialized.
cleanTestData = (firestore, path) => {
console.log("Cleaning-up old test data")
var query = firestore.collection(path)
return query.get().then(snapshot => {
const deletePromises = []
if (snapshot.size > 0) {
snapshot.docs.forEach(function(doc) {
deletePromises.push(doc.ref.delete().then(() => {
console.log("Deleted ", doc.id)
}))
});
}
return Promise.all(deletePromises)
}).then(() => {
console.log("Old test data cleaned-up")
})
}
createTestData = (firestore, path) => {
console.log("Creating test data")
const batch = firestore.batch()
const data = {
a: '2017-09-02',
b: '2017-12-25',
c: '2017-10-06',
d: '2017-08-02',
e: '2017-09-20',
f: '2017-11-17'
}
for (const id in data) {
batch.set(firestore.collection(path).doc(id), { date: data[id] })
}
return batch.commit().then(() => {
console.log("Test data created");
}).catch(error => {
console.error("Failed to create test data: ", error);
})
}
subscribe = (firestore, path) => {
const datesArray = []
return firestore.collection(path).orderBy('date', 'asc').onSnapshot(snapshot => {
snapshot.docChanges.forEach(change => {
console.log(change.type, "id:", change.doc.id,
"; date:", change.doc.data().date,
"; oldIndex:", change.oldIndex, "; newIndex:", change.newIndex,
"; metadata: ", change.doc.metadata)
if (change.oldIndex !== -1) {
datesArray.splice(change.oldIndex, 1);
}
if (change.newIndex !== -1) {
datesArray.splice(change.newIndex, 0, change.doc.data().date);
}
console.log(" -->", JSON.stringify(datesArray))
})
})
}
update = (firestore, path) => {
console.log("Updating test data")
return firestore.collection(path).doc('d').set({date: '2018-01-02'}).then(() => {
console.log("Test doc 'd' updated from '2017-08-02' to '2018-01-02'")
})
}
query = (firestore, path) => {
var query = firestore.collection(path).orderBy('date', 'asc')
return query.get().then(snapshot => {
const dates = []
if (snapshot.size > 0) {
snapshot.docs.forEach(function(doc) {
dates.push(doc.data().date)
});
}
console.log("Fresh query of data: \n -->", JSON.stringify(dates))
})
}
handleStartTest = e => {
console.log("Starting test")
const firestore = firebase.firestore()
const path = `things`
let unsubscribeFn = null
unsubscribeFn = this.subscribe(firestore, path)
this.cleanTestData(firestore, path).then(() => {
return this.createTestData(firestore, path)
}).then(() => {
return this.update(firestore, path)
}).then(() => {
return this.query(firestore, path)
}).then(() => {
unsubscribeFn()
console.log("Test complete")
}).catch((error) => {
console.error("Test failed: ", error)
})
}
This is the way it worked for me:
onSnapshot((ref) => {
ref.docChanges().forEach((change) => {
const { newIndex, oldIndex, doc, type } = change;
if (type === 'added') {
this.todos.splice(newIndex, 0, doc.data());
// if we want to handle references we would do it here
} else if (type === 'modified') {
// remove the old one first
this.todos.splice(oldIndex, 1);
// if we want to handle references we would have to unsubscribe
// from old references' listeners and subscribe to the new ones
this.todos.splice(newIndex, 0, doc.data());
} else if (type === 'removed') {
this.todos.splice(oldIndex, 1);
// if we want to handle references we need to unsubscribe
// from old references
}
});
});
source