C
C#2y ago
Hulkstance

❔ React Redux SignalR

I'm trying to implement an auto-refreshing datagrid (Ant Design Table). Is there a good article on how to implement the React part?
public sealed class StockTickerHub : Hub
{
private readonly IStockTickerService _stockTickerService;

public StockTickerHub(IStockTickerService stockTickerService)
{
_stockTickerService = stockTickerService;
}

public ChannelReader<Stock> GetStockTickerStream(CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<Stock>();

_ = _stockTickerService.SubscribeAsync(channel.Writer, cancellationToken);

return channel.Reader;
}
}

public class Stock
{
public required string Symbol { get; init; }

public required double Price { get; init; }
}

public interface IStockTickerService
{
Task SubscribeAsync(ChannelWriter<Stock> writer, CancellationToken cancellationToken);
}

public sealed class StockTickerService : IStockTickerService
{
public async Task SubscribeAsync(ChannelWriter<Stock> writer, CancellationToken cancellationToken)
{
try
{
while (true)
{
var stock = new Stock
{
Symbol = "TESLA",
Price = Math.Round(Random.Shared.NextDouble() * 100, 2)
};
await writer.WriteAsync(stock, cancellationToken);
await Task.Delay(1000, cancellationToken);
}
}
finally
{
writer.Complete();
}
}
}
public sealed class StockTickerHub : Hub
{
private readonly IStockTickerService _stockTickerService;

public StockTickerHub(IStockTickerService stockTickerService)
{
_stockTickerService = stockTickerService;
}

public ChannelReader<Stock> GetStockTickerStream(CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<Stock>();

_ = _stockTickerService.SubscribeAsync(channel.Writer, cancellationToken);

return channel.Reader;
}
}

public class Stock
{
public required string Symbol { get; init; }

public required double Price { get; init; }
}

public interface IStockTickerService
{
Task SubscribeAsync(ChannelWriter<Stock> writer, CancellationToken cancellationToken);
}

public sealed class StockTickerService : IStockTickerService
{
public async Task SubscribeAsync(ChannelWriter<Stock> writer, CancellationToken cancellationToken)
{
try
{
while (true)
{
var stock = new Stock
{
Symbol = "TESLA",
Price = Math.Round(Random.Shared.NextDouble() * 100, 2)
};
await writer.WriteAsync(stock, cancellationToken);
await Task.Delay(1000, cancellationToken);
}
}
finally
{
writer.Complete();
}
}
}
2 Replies
Hulkstance
Hulkstance2y ago
configureStore.ts
import { configureStore } from "@reduxjs/toolkit";
import stockReducer from "./stockSlice";

export const store = configureStore({
reducer: {
stock: stockReducer,
},
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;

// Inferred type: {stock: StockState}
export type AppDispatch = typeof store.dispatch;
import { configureStore } from "@reduxjs/toolkit";
import stockReducer from "./stockSlice";

export const store = configureStore({
reducer: {
stock: stockReducer,
},
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;

// Inferred type: {stock: StockState}
export type AppDispatch = typeof store.dispatch;
stockSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export type Stock = Readonly<{
symbol: string;
price: number;
}>;

export type StockState = Readonly<{
stocks: Stock[];
}>;

const initialState: StockState = {
stocks: [],
};

const stockSlice = createSlice({
name: "stock",
initialState: initialState,
reducers: {
addStock(state, action: PayloadAction<Stock>) {
state.stocks.push(action.payload);
},
},
});

export const { addStock } = stockSlice.actions;

export default stockSlice.reducer;
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export type Stock = Readonly<{
symbol: string;
price: number;
}>;

export type StockState = Readonly<{
stocks: Stock[];
}>;

const initialState: StockState = {
stocks: [],
};

const stockSlice = createSlice({
name: "stock",
initialState: initialState,
reducers: {
addStock(state, action: PayloadAction<Stock>) {
state.stocks.push(action.payload);
},
},
});

export const { addStock } = stockSlice.actions;

export default stockSlice.reducer;
hooks.ts
import type { RootState, AppDispatch } from './configureStore';
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
import type { RootState, AppDispatch } from './configureStore';
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
import {
HttpTransportType,
HubConnectionBuilder,
LogLevel,
} from "@microsoft/signalr";
import { Table } from "antd";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { addStock, Stock } from "../store/stockSlice";

const connection = new HubConnectionBuilder()
.withUrl("https://localhost:5001/stockticker", {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
})
.configureLogging(LogLevel.Information)
.withAutomaticReconnect()
.build();

const Grid = () => {
const dispatch = useDispatch();
const [stocks, setStocks] = useState<Stock[]>([]);

useEffect(() => {
async function start() {
try {
await connection.start();

const reader = await connection.invoke("getStockTickerStream");
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
dispatch(addStock(value));
setStocks((prevStocks) => [...prevStocks, value]);
}
} catch (err) {
console.error(err);
}
}

start();

return () => {
// connection.stop();
};
}, [dispatch]);

return (
<Table dataSource={stocks}>
<Table.Column title="Symbol" dataIndex="symbol" key="symbol" />
<Table.Column title="Price" dataIndex="price" key="price" />
</Table>
);
};

export default Grid;
import {
HttpTransportType,
HubConnectionBuilder,
LogLevel,
} from "@microsoft/signalr";
import { Table } from "antd";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { addStock, Stock } from "../store/stockSlice";

const connection = new HubConnectionBuilder()
.withUrl("https://localhost:5001/stockticker", {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
})
.configureLogging(LogLevel.Information)
.withAutomaticReconnect()
.build();

const Grid = () => {
const dispatch = useDispatch();
const [stocks, setStocks] = useState<Stock[]>([]);

useEffect(() => {
async function start() {
try {
await connection.start();

const reader = await connection.invoke("getStockTickerStream");
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
dispatch(addStock(value));
setStocks((prevStocks) => [...prevStocks, value]);
}
} catch (err) {
console.error(err);
}
}

start();

return () => {
// connection.stop();
};
}, [dispatch]);

return (
<Table dataSource={stocks}>
<Table.Column title="Symbol" dataIndex="symbol" key="symbol" />
<Table.Column title="Price" dataIndex="price" key="price" />
</Table>
);
};

export default Grid;
How do I spawn the SignalR connection correctly and maintain it? Grid.tsx is the problematic code. It's completely wrong https://github.com/fernetowac/settlechat/blob/86233d4ae762f54fbeccd87cdf62e325b43edb4b/SettleChat/ClientApp/src/store/SignalR.ts https://github.com/fernetowac/settlechat/blob/86233d4ae762f54fbeccd87cdf62e325b43edb4b/SettleChat/ClientApp/src/components/SignalRContainer.tsx this might be helpful but dunno
Accord
Accord2y ago
Looks like nothing has happened here. I will mark this as stale and this post will be archived until there is new activity.